From ba3ae16141d887a7a4f8d067384ad84126a7936b Mon Sep 17 00:00:00 2001 From: Paul Cook Date: Tue, 25 Jul 2017 16:19:09 +0100 Subject: [PATCH 01/19] add support for supplying multiple public keys that will be attempted when validating a token signature this facilitates the supporting of certificate rotation, and there being multiple valid keys available during a rotation cycle --- .../io/jsonwebtoken/SigningKeyResolver.java | 24 +++++ .../SigningKeyResolverAdapter.java | 71 ++++++++++++++ .../jsonwebtoken/impl/DefaultJwtParser.java | 98 +++++++++++-------- .../crypto/DefaultJwtSignatureValidator.java | 9 +- .../DefaultSignatureValidatorFactory.java | 11 ++- .../EllipticCurveSignatureValidator.java | 45 +++++---- .../impl/crypto/MacValidator.java | 20 ++-- .../impl/crypto/RsaSignatureValidator.java | 59 +++++++---- .../crypto/SignatureValidatorFactory.java | 3 +- .../java/io/jsonwebtoken/lang/Objects.java | 11 +++ 10 files changed, 260 insertions(+), 91 deletions(-) diff --git a/src/main/java/io/jsonwebtoken/SigningKeyResolver.java b/src/main/java/io/jsonwebtoken/SigningKeyResolver.java index b068db9d2..789a9c2c8 100644 --- a/src/main/java/io/jsonwebtoken/SigningKeyResolver.java +++ b/src/main/java/io/jsonwebtoken/SigningKeyResolver.java @@ -16,6 +16,7 @@ package io.jsonwebtoken; import java.security.Key; +import java.util.Collection; /** * A {@code SigningKeyResolver} can be used by a {@link io.jsonwebtoken.JwtParser JwtParser} to find a signing key that @@ -60,6 +61,17 @@ public interface SigningKeyResolver { */ Key resolveSigningKey(JwsHeader header, Claims claims); + /** + * Returns a collection signing key that should be used to attempt to validate a digital signature for the Claims JWS with the specified + * header and claims. This allows for a key rotation scenario to support multiple keys during an overlap period. + * + * @param header the header of the JWS to validate + * @param claims the claims (body) of the JWS to validate + * @return the signing key that should be used to validate a digital signature for the Claims JWS with the specified + * header and claims. + */ + Collection resolveSigningKeys(JwsHeader header, Claims claims); + /** * Returns the signing key that should be used to validate a digital signature for the Plaintext JWS with the * specified header and plaintext payload. @@ -70,4 +82,16 @@ public interface SigningKeyResolver { * specified header and plaintext payload. */ Key resolveSigningKey(JwsHeader header, String plaintext); + + /** + * Returns the signing key that should be used to attempt to validate a digital signature for the Plaintext JWS with the + * specified header and plaintext payload. This allows for a key rotation scenario to support multiple keys during an overlap period. + * + * @param header the header of the JWS to validate + * @param plaintext the plaintext body of the JWS to validate + * @return the signing key that should be used to validate a digital signature for the Plaintext JWS with the + * specified header and plaintext payload. + */ + Collection resolveSigningKeys(JwsHeader header, String plaintext); + } diff --git a/src/main/java/io/jsonwebtoken/SigningKeyResolverAdapter.java b/src/main/java/io/jsonwebtoken/SigningKeyResolverAdapter.java index 1be7ec556..dcedaf5d3 100644 --- a/src/main/java/io/jsonwebtoken/SigningKeyResolverAdapter.java +++ b/src/main/java/io/jsonwebtoken/SigningKeyResolverAdapter.java @@ -19,6 +19,8 @@ import javax.crypto.spec.SecretKeySpec; import java.security.Key; +import java.util.ArrayList; +import java.util.Collection; /** * An Adapter implementation of the @@ -51,6 +53,23 @@ public Key resolveSigningKey(JwsHeader header, Claims claims) { return new SecretKeySpec(keyBytes, alg.getJcaName()); } + @Override + public Collection resolveSigningKeys(JwsHeader header, Claims claims) { + SignatureAlgorithm alg = SignatureAlgorithm.forName(header.getAlgorithm()); + Assert.isTrue(alg.isHmac(), "The default resolveSigningKey(JwsHeader, Claims) implementation cannot be " + + "used for asymmetric key algorithms (RSA, Elliptic Curve). " + + "Override the resolveSigningKey(JwsHeader, Claims) method instead and return a " + + "Key instance appropriate for the " + alg.name() + " algorithm."); + Collection keysBytes = resolveSigningKeysBytes(header, claims); + if (keysBytes == null) + return null; + Collection keys = new ArrayList<>(); + for (byte[] keyBytes: keysBytes) + keys.add(new SecretKeySpec(keyBytes, alg.getJcaName())); + + return keys; + } + @Override public Key resolveSigningKey(JwsHeader header, String plaintext) { SignatureAlgorithm alg = SignatureAlgorithm.forName(header.getAlgorithm()); @@ -62,6 +81,23 @@ public Key resolveSigningKey(JwsHeader header, String plaintext) { return new SecretKeySpec(keyBytes, alg.getJcaName()); } + @Override + public Collection resolveSigningKeys(JwsHeader header, String plaintext) { + SignatureAlgorithm alg = SignatureAlgorithm.forName(header.getAlgorithm()); + Assert.isTrue(alg.isHmac(), "The default resolveSigningKey(JwsHeader, String) implementation cannot be " + + "used for asymmetric key algorithms (RSA, Elliptic Curve). " + + "Override the resolveSigningKey(JwsHeader, String) method instead and return a " + + "Key instance appropriate for the " + alg.name() + " algorithm."); + Collection keysBytes = resolveSigningKeysBytes(header, plaintext); + if (keysBytes == null) + return null; + Collection keys = new ArrayList<>(); + for (byte[] keyBytes: keysBytes) + keys.add(new SecretKeySpec(keyBytes, alg.getJcaName())); + + return keys; + } + /** * Convenience method invoked by {@link #resolveSigningKey(JwsHeader, Claims)} that obtains the necessary signing * key bytes. This implementation simply throws an exception: if the JWS parsed is a Claims JWS, you must @@ -81,6 +117,25 @@ public byte[] resolveSigningKeyBytes(JwsHeader header, Claims claims) { "resolveSigningKeyBytes(JwsHeader, Claims) method."); } + /** + * Convenience method invoked by {@link #resolveSigningKey(JwsHeader, Claims)} that obtains the necessary signing + * key bytes. This implementation simply throws an exception: if the JWS parsed is a Claims JWS, you must + * override this method or the {@link #resolveSigningKey(JwsHeader, Claims)} method instead. + * + *

NOTE: You cannot override this method when validating RSA signatures. If you expect RSA signatures, + * you must override the {@link #resolveSigningKey(JwsHeader, Claims)} method instead.

+ * + * @param header the parsed {@link JwsHeader} + * @param claims the parsed {@link Claims} + * @return the signing key bytes to use to verify the JWS signature. + */ + public Collection resolveSigningKeysBytes(JwsHeader header, Claims claims) { + throw new UnsupportedJwtException("The specified SigningKeyResolver implementation does not support " + + "Claims JWS signing key resolution. Consider overriding either the " + + "resolveSigningKey(JwsHeader, Claims) method or, for HMAC algorithms, the " + + "resolveSigningKeyBytes(JwsHeader, Claims) method."); + } + /** * Convenience method invoked by {@link #resolveSigningKey(JwsHeader, String)} that obtains the necessary signing * key bytes. This implementation simply throws an exception: if the JWS parsed is a plaintext JWS, you must @@ -96,4 +151,20 @@ public byte[] resolveSigningKeyBytes(JwsHeader header, String payload) { "resolveSigningKey(JwsHeader, String) method or, for HMAC algorithms, the " + "resolveSigningKeyBytes(JwsHeader, String) method."); } + + /** + * Convenience method invoked by {@link #resolveSigningKey(JwsHeader, String)} that obtains the necessary signing + * key bytes. This implementation simply throws an exception: if the JWS parsed is a plaintext JWS, you must + * override this method or the {@link #resolveSigningKey(JwsHeader, String)} method instead. + * + * @param header the parsed {@link JwsHeader} + * @param payload the parsed String plaintext payload + * @return the signing key bytes to use to verify the JWS signature. + */ + public Collection resolveSigningKeysBytes(JwsHeader header, String payload) { + throw new UnsupportedJwtException("The specified SigningKeyResolver implementation does not support " + + "plaintext JWS signing key resolution. Consider overriding either the " + + "resolveSigningKey(JwsHeader, String) method or, for HMAC algorithms, the " + + "resolveSigningKeyBytes(JwsHeader, String) method."); + } } diff --git a/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java b/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java index 4e4b9c79c..4cc81d7d2 100644 --- a/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java +++ b/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java @@ -49,6 +49,8 @@ import java.io.IOException; import java.security.Key; import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Collection; import java.util.Date; import java.util.Map; @@ -59,11 +61,11 @@ public class DefaultJwtParser implements JwtParser { private static final String ISO_8601_FORMAT = "yyyy-MM-dd'T'HH:mm:ss'Z'"; private static final int MILLISECONDS_PER_SECOND = 1000; - private ObjectMapper objectMapper = new ObjectMapper(); + private final ObjectMapper objectMapper = new ObjectMapper(); - private byte[] keyBytes; + private Collection keyBytes; - private Key key; + private Collection keys; private SigningKeyResolver signingKeyResolver; @@ -77,43 +79,43 @@ public class DefaultJwtParser implements JwtParser { @Override public JwtParser requireIssuedAt(Date issuedAt) { - expectedClaims.setIssuedAt(issuedAt); + this.expectedClaims.setIssuedAt(issuedAt); return this; } @Override public JwtParser requireIssuer(String issuer) { - expectedClaims.setIssuer(issuer); + this.expectedClaims.setIssuer(issuer); return this; } @Override public JwtParser requireAudience(String audience) { - expectedClaims.setAudience(audience); + this.expectedClaims.setAudience(audience); return this; } @Override public JwtParser requireSubject(String subject) { - expectedClaims.setSubject(subject); + this.expectedClaims.setSubject(subject); return this; } @Override public JwtParser requireId(String id) { - expectedClaims.setId(id); + this.expectedClaims.setId(id); return this; } @Override public JwtParser requireExpiration(Date expiration) { - expectedClaims.setExpiration(expiration); + this.expectedClaims.setExpiration(expiration); return this; } @Override public JwtParser requireNotBefore(Date notBefore) { - expectedClaims.setNotBefore(notBefore); + this.expectedClaims.setNotBefore(notBefore); return this; } @@ -121,7 +123,7 @@ public JwtParser requireNotBefore(Date notBefore) { public JwtParser require(String claimName, Object value) { Assert.hasText(claimName, "claim name cannot be null or empty."); Assert.notNull(value, "The value cannot be null for claim name: " + claimName); - expectedClaims.put(claimName, value); + this.expectedClaims.put(claimName, value); return this; } @@ -141,21 +143,27 @@ public JwtParser setAllowedClockSkewSeconds(long seconds) { @Override public JwtParser setSigningKey(byte[] key) { Assert.notEmpty(key, "signing key cannot be null or empty."); - this.keyBytes = key; + if (this.keyBytes == null) + this.keyBytes = new ArrayList<>(); + this.keyBytes.add(key); return this; } @Override public JwtParser setSigningKey(String base64EncodedKeyBytes) { Assert.hasText(base64EncodedKeyBytes, "signing key cannot be null or empty."); - this.keyBytes = TextCodec.BASE64.decode(base64EncodedKeyBytes); + if (this.keyBytes == null) + this.keyBytes = new ArrayList<>(); + this.keyBytes.add(TextCodec.BASE64.decode(base64EncodedKeyBytes)); return this; } @Override public JwtParser setSigningKey(Key key) { Assert.notNull(key, "signing key cannot be null."); - this.key = key; + if (this.keys == null) + this.keys = new ArrayList<>(); + this.keys.add(key); return this; } @@ -257,7 +265,7 @@ public Jwt parse(String jwt) throws ExpiredJwtException, MalformedJwtException, header = new DefaultHeader(m); } - compressionCodec = compressionCodecResolver.resolveCompressionCodec(header); + compressionCodec = this.compressionCodecResolver.resolveCompressionCodec(header); } // =============== Body ================= @@ -297,47 +305,59 @@ public Jwt parse(String jwt) throws ExpiredJwtException, MalformedJwtException, throw new MalformedJwtException(msg); } - if (key != null && keyBytes != null) { + if (this.keys != null && this.keyBytes != null) { throw new IllegalStateException("A key object and key bytes cannot both be specified. Choose either."); - } else if ((key != null || keyBytes != null) && signingKeyResolver != null) { - String object = key != null ? "a key object" : "key bytes"; + } else if ((this.keys != null || this.keyBytes != null) && this.signingKeyResolver != null) { + String object = this.keys != null ? "a key object" : "key bytes"; throw new IllegalStateException("A signing key resolver and " + object + " cannot both be specified. Choose either."); } //digitally signed, let's assert the signature: - Key key = this.key; + Collection keys = this.keys; - if (key == null) { //fall back to keyBytes + if (keys == null) { //fall back to keyBytes - byte[] keyBytes = this.keyBytes; - - if (Objects.isEmpty(keyBytes) && signingKeyResolver != null) { //use the signingKeyResolver + if (Objects.isEmpty(this.keyBytes) && this.signingKeyResolver != null) { //use the signingKeyResolver + keys = new ArrayList<>(); if (claims != null) { - key = signingKeyResolver.resolveSigningKey(jwsHeader, claims); + Key key = this.signingKeyResolver.resolveSigningKey(jwsHeader, claims); + if (key != null) + keys.add(key); + Collection keyList = this.signingKeyResolver.resolveSigningKeys(jwsHeader, claims); + if (!Objects.isEmpty(keyList)) + keys.addAll(keyList); } else { - key = signingKeyResolver.resolveSigningKey(jwsHeader, payload); + Key key = this.signingKeyResolver.resolveSigningKey(jwsHeader, payload); + if (key != null) + keys.add(key); + Collection keyList = this.signingKeyResolver.resolveSigningKeys(jwsHeader, payload); + if (!Objects.isEmpty(keyList)) + keys.addAll(keyList); } } - if (!Objects.isEmpty(keyBytes)) { + if (!Objects.isEmpty(this.keyBytes)) { Assert.isTrue(algorithm.isHmac(), "Key bytes can only be specified for HMAC signatures. Please specify a PublicKey or PrivateKey instance."); - key = new SecretKeySpec(keyBytes, algorithm.getJcaName()); + keys = new ArrayList<>(); + for (byte[] bytes: this.keyBytes) + this.keys.add(new SecretKeySpec(bytes, algorithm.getJcaName())); } } - Assert.notNull(key, "A signing key must be specified if the specified JWT is digitally signed."); + Assert.notNull(keys, "A signing key must be specified if the specified JWT is digitally signed."); //re-create the jwt part without the signature. This is what needs to be signed for verification: String jwtWithoutSignature = base64UrlEncodedHeader + SEPARATOR_CHAR + base64UrlEncodedPayload; JwtSignatureValidator validator; try { - validator = createSignatureValidator(algorithm, key); + validator = createSignatureValidator(algorithm, keys); } catch (IllegalArgumentException e) { String algName = algorithm.getValue(); + Key key = keys.iterator().next(); String msg = "The parsed JWT indicates it was signed with the " + algName + " signature " + "algorithm, but the specified signing key of type " + key.getClass().getName() + " may not be used to validate " + algName + " signatures. Because the specified " + @@ -421,9 +441,9 @@ public Jwt parse(String jwt) throws ExpiredJwtException, MalformedJwtException, } private void validateExpectedClaims(Header header, Claims claims) { - for (String expectedClaimName : expectedClaims.keySet()) { + for (String expectedClaimName : this.expectedClaims.keySet()) { - Object expectedClaimValue = expectedClaims.get(expectedClaimName); + Object expectedClaimValue = this.expectedClaims.get(expectedClaimName); Object actualClaimValue = claims.get(expectedClaimName); if ( @@ -431,7 +451,7 @@ private void validateExpectedClaims(Header header, Claims claims) { Claims.EXPIRATION.equals(expectedClaimName) || Claims.NOT_BEFORE.equals(expectedClaimName) ) { - expectedClaimValue = expectedClaims.get(expectedClaimName, Date.class); + expectedClaimValue = this.expectedClaims.get(expectedClaimName, Date.class); actualClaimValue = claims.get(expectedClaimName, Date.class); } else if ( expectedClaimValue instanceof Date && @@ -468,8 +488,8 @@ private void validateExpectedClaims(Header header, Claims claims) { /* * @since 0.5 mostly to allow testing overrides */ - protected JwtSignatureValidator createSignatureValidator(SignatureAlgorithm alg, Key key) { - return new DefaultJwtSignatureValidator(alg, key); + protected JwtSignatureValidator createSignatureValidator(SignatureAlgorithm alg, Collection keys) { + return new DefaultJwtSignatureValidator(alg, keys); } @Override @@ -484,16 +504,16 @@ public T parse(String compact, JwtHandler handler) Jws jws = (Jws) jwt; Object body = jws.getBody(); if (body instanceof Claims) { - return handler.onClaimsJws((Jws) jws); + return handler.onClaimsJws(jws); } else { - return handler.onPlaintextJws((Jws) jws); + return handler.onPlaintextJws(jws); } } else { Object body = jwt.getBody(); if (body instanceof Claims) { - return handler.onClaimsJwt((Jwt) jwt); + return handler.onClaimsJwt(jwt); } else { - return handler.onPlaintextJwt((Jwt) jwt); + return handler.onPlaintextJwt(jwt); } } } @@ -549,7 +569,7 @@ public Jws onClaimsJws(Jws jws) { @SuppressWarnings("unchecked") protected Map readValue(String val) { try { - return objectMapper.readValue(val, Map.class); + return this.objectMapper.readValue(val, Map.class); } catch (IOException e) { throw new MalformedJwtException("Unable to read JSON value: " + val, e); } diff --git a/src/main/java/io/jsonwebtoken/impl/crypto/DefaultJwtSignatureValidator.java b/src/main/java/io/jsonwebtoken/impl/crypto/DefaultJwtSignatureValidator.java index 245c0d551..0f48a5d32 100644 --- a/src/main/java/io/jsonwebtoken/impl/crypto/DefaultJwtSignatureValidator.java +++ b/src/main/java/io/jsonwebtoken/impl/crypto/DefaultJwtSignatureValidator.java @@ -21,6 +21,7 @@ import java.nio.charset.Charset; import java.security.Key; +import java.util.Collection; public class DefaultJwtSignatureValidator implements JwtSignatureValidator { @@ -28,13 +29,13 @@ public class DefaultJwtSignatureValidator implements JwtSignatureValidator { private final SignatureValidator signatureValidator; - public DefaultJwtSignatureValidator(SignatureAlgorithm alg, Key key) { - this(DefaultSignatureValidatorFactory.INSTANCE, alg, key); + public DefaultJwtSignatureValidator(SignatureAlgorithm alg, Collection keys) { + this(DefaultSignatureValidatorFactory.INSTANCE, alg, keys); } - public DefaultJwtSignatureValidator(SignatureValidatorFactory factory, SignatureAlgorithm alg, Key key) { + public DefaultJwtSignatureValidator(SignatureValidatorFactory factory, SignatureAlgorithm alg, Collection keys) { Assert.notNull(factory, "SignerFactory argument cannot be null."); - this.signatureValidator = factory.createSignatureValidator(alg, key); + this.signatureValidator = factory.createSignatureValidator(alg, keys); } @Override diff --git a/src/main/java/io/jsonwebtoken/impl/crypto/DefaultSignatureValidatorFactory.java b/src/main/java/io/jsonwebtoken/impl/crypto/DefaultSignatureValidatorFactory.java index 82916847c..be909c6f5 100644 --- a/src/main/java/io/jsonwebtoken/impl/crypto/DefaultSignatureValidatorFactory.java +++ b/src/main/java/io/jsonwebtoken/impl/crypto/DefaultSignatureValidatorFactory.java @@ -19,32 +19,33 @@ import io.jsonwebtoken.lang.Assert; import java.security.Key; +import java.util.Collection; public class DefaultSignatureValidatorFactory implements SignatureValidatorFactory { public static final SignatureValidatorFactory INSTANCE = new DefaultSignatureValidatorFactory(); @Override - public SignatureValidator createSignatureValidator(SignatureAlgorithm alg, Key key) { + public SignatureValidator createSignatureValidator(SignatureAlgorithm alg, Collection keys) { Assert.notNull(alg, "SignatureAlgorithm cannot be null."); - Assert.notNull(key, "Signing Key cannot be null."); + Assert.notNull(keys, "Signing Key cannot be null."); switch (alg) { case HS256: case HS384: case HS512: - return new MacValidator(alg, key); + return new MacValidator(alg, keys); case RS256: case RS384: case RS512: case PS256: case PS384: case PS512: - return new RsaSignatureValidator(alg, key); + return new RsaSignatureValidator(alg, keys); case ES256: case ES384: case ES512: - return new EllipticCurveSignatureValidator(alg, key); + return new EllipticCurveSignatureValidator(alg, keys); default: throw new IllegalArgumentException("The '" + alg.name() + "' algorithm cannot be used for signing."); } diff --git a/src/main/java/io/jsonwebtoken/impl/crypto/EllipticCurveSignatureValidator.java b/src/main/java/io/jsonwebtoken/impl/crypto/EllipticCurveSignatureValidator.java index 09ab14db6..ed548e37f 100644 --- a/src/main/java/io/jsonwebtoken/impl/crypto/EllipticCurveSignatureValidator.java +++ b/src/main/java/io/jsonwebtoken/impl/crypto/EllipticCurveSignatureValidator.java @@ -20,6 +20,7 @@ import java.security.PublicKey; import java.security.Signature; import java.security.interfaces.ECPublicKey; +import java.util.Collection; import io.jsonwebtoken.SignatureAlgorithm; import io.jsonwebtoken.SignatureException; @@ -30,30 +31,38 @@ public class EllipticCurveSignatureValidator extends EllipticCurveProvider imple private static final String EC_PUBLIC_KEY_REQD_MSG = "Elliptic Curve signature validation requires an ECPublicKey instance."; - public EllipticCurveSignatureValidator(SignatureAlgorithm alg, Key key) { - super(alg, key); - Assert.isTrue(key instanceof ECPublicKey, EC_PUBLIC_KEY_REQD_MSG); + private final Collection keys; + + public EllipticCurveSignatureValidator(SignatureAlgorithm alg, Collection keys) { + super(alg, null); + this.keys = keys; + for (Key key: keys) + Assert.isTrue(key instanceof ECPublicKey, EC_PUBLIC_KEY_REQD_MSG); } @Override public boolean isValid(byte[] data, byte[] signature) { Signature sig = createSignatureInstance(); - PublicKey publicKey = (PublicKey) key; - try { - int expectedSize = getSignatureByteArrayLength(alg); - /** - * - * If the expected size is not valid for JOSE, fall back to ASN.1 DER signature. - * This fallback is for backwards compatibility ONLY (to support tokens generated by previous versions of jjwt) - * and backwards compatibility will possibly be removed in a future version of this library. - * - * **/ - byte[] derSignature = expectedSize != signature.length && signature[0] == 0x30 ? signature : EllipticCurveProvider.transcodeSignatureToDER(signature); - return doVerify(sig, publicKey, data, derSignature); - } catch (Exception e) { - String msg = "Unable to verify Elliptic Curve signature using configured ECPublicKey. " + e.getMessage(); - throw new SignatureException(msg, e); + for (Key key: this.keys) { + PublicKey publicKey = (PublicKey) key; + try { + int expectedSize = getSignatureByteArrayLength(this.alg); + /** + * + * If the expected size is not valid for JOSE, fall back to ASN.1 DER signature. + * This fallback is for backwards compatibility ONLY (to support tokens generated by previous versions of jjwt) + * and backwards compatibility will possibly be removed in a future version of this library. + * + * **/ + byte[] derSignature = expectedSize != signature.length && signature[0] == 0x30 ? signature : EllipticCurveProvider.transcodeSignatureToDER(signature); + if (doVerify(sig, publicKey, data, derSignature)) + return true; + } catch (Exception e) { + String msg = "Unable to verify Elliptic Curve signature using configured ECPublicKey. " + e.getMessage(); + throw new SignatureException(msg, e); + } } + return false; } protected boolean doVerify(Signature sig, PublicKey publicKey, byte[] data, byte[] signature) diff --git a/src/main/java/io/jsonwebtoken/impl/crypto/MacValidator.java b/src/main/java/io/jsonwebtoken/impl/crypto/MacValidator.java index af3196661..ee5e3a314 100644 --- a/src/main/java/io/jsonwebtoken/impl/crypto/MacValidator.java +++ b/src/main/java/io/jsonwebtoken/impl/crypto/MacValidator.java @@ -19,19 +19,27 @@ import java.security.Key; import java.security.MessageDigest; -import java.util.Arrays; +import java.util.ArrayList; +import java.util.Collection; public class MacValidator implements SignatureValidator { - private final MacSigner signer; + private final Collection signers; - public MacValidator(SignatureAlgorithm alg, Key key) { - this.signer = new MacSigner(alg, key); + public MacValidator(SignatureAlgorithm alg, Collection keys) { + Collection signers = new ArrayList<>(); + for (Key key: keys) + signers.add(new MacSigner(alg, key)); + this.signers = signers; } @Override public boolean isValid(byte[] data, byte[] signature) { - byte[] computed = this.signer.sign(data); - return MessageDigest.isEqual(computed, signature); + for (MacSigner signer: this.signers) { + byte[] computed = signer.sign(data); + if (MessageDigest.isEqual(computed, signature)) + return true; + } + return false; } } diff --git a/src/main/java/io/jsonwebtoken/impl/crypto/RsaSignatureValidator.java b/src/main/java/io/jsonwebtoken/impl/crypto/RsaSignatureValidator.java index a306d748f..f29f9082e 100644 --- a/src/main/java/io/jsonwebtoken/impl/crypto/RsaSignatureValidator.java +++ b/src/main/java/io/jsonwebtoken/impl/crypto/RsaSignatureValidator.java @@ -25,35 +25,58 @@ import java.security.Signature; import java.security.interfaces.RSAPrivateKey; import java.security.interfaces.RSAPublicKey; +import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; public class RsaSignatureValidator extends RsaProvider implements SignatureValidator { - private final RsaSigner SIGNER; + private final Collection SIGNERS; - public RsaSignatureValidator(SignatureAlgorithm alg, Key key) { - super(alg, key); - Assert.isTrue(key instanceof RSAPrivateKey || key instanceof RSAPublicKey, - "RSA Signature validation requires either a RSAPublicKey or RSAPrivateKey instance."); - this.SIGNER = key instanceof RSAPrivateKey ? new RsaSigner(alg, key) : null; + public static final class SignerAndKey { + + private final RsaSigner signer; + private final Key key; + + public SignerAndKey(final RsaSigner signer, final Key key) { + this.signer = signer; + this.key = key; + } + } + + public RsaSignatureValidator(SignatureAlgorithm alg, Collection keys) { + super(alg, null); + + Collection SIGNERS = new ArrayList<>(); + for (Key key: keys) { + Assert.isTrue(key instanceof RSAPrivateKey || key instanceof RSAPublicKey, + "RSA Signature validation requires either a RSAPublicKey or RSAPrivateKey instance."); + SIGNERS.add(new SignerAndKey(new RsaSigner(alg, key), key)); + } + this.SIGNERS = SIGNERS; } @Override public boolean isValid(byte[] data, byte[] signature) { - if (key instanceof PublicKey) { - Signature sig = createSignatureInstance(); - PublicKey publicKey = (PublicKey) key; - try { - return doVerify(sig, publicKey, data, signature); - } catch (Exception e) { - String msg = "Unable to verify RSA signature using configured PublicKey. " + e.getMessage(); - throw new SignatureException(msg, e); + for (SignerAndKey signerAndKey: this.SIGNERS) { + if (signerAndKey.key instanceof PublicKey) { + Signature sig = createSignatureInstance(); + PublicKey publicKey = (PublicKey) signerAndKey.key; + try { + if (doVerify(sig, publicKey, data, signature)) + return true; + } catch (Exception e) { + String msg = "Unable to verify RSA signature using configured PublicKey. " + e.getMessage(); + throw new SignatureException(msg, e); + } + } else { + Assert.notNull(this.SIGNERS, "RSA Signer instance cannot be null. This is a bug. Please report it."); + byte[] computed = signerAndKey.signer.sign(data); + if (Arrays.equals(computed, signature)) + return true; } - } else { - Assert.notNull(this.SIGNER, "RSA Signer instance cannot be null. This is a bug. Please report it."); - byte[] computed = this.SIGNER.sign(data); - return Arrays.equals(computed, signature); } + return false; } protected boolean doVerify(Signature sig, PublicKey publicKey, byte[] data, byte[] signature) diff --git a/src/main/java/io/jsonwebtoken/impl/crypto/SignatureValidatorFactory.java b/src/main/java/io/jsonwebtoken/impl/crypto/SignatureValidatorFactory.java index 1e84b620e..7712f4a4d 100644 --- a/src/main/java/io/jsonwebtoken/impl/crypto/SignatureValidatorFactory.java +++ b/src/main/java/io/jsonwebtoken/impl/crypto/SignatureValidatorFactory.java @@ -18,8 +18,9 @@ import io.jsonwebtoken.SignatureAlgorithm; import java.security.Key; +import java.util.Collection; public interface SignatureValidatorFactory { - SignatureValidator createSignatureValidator(SignatureAlgorithm alg, Key key); + SignatureValidator createSignatureValidator(SignatureAlgorithm alg, Collection keys); } diff --git a/src/main/java/io/jsonwebtoken/lang/Objects.java b/src/main/java/io/jsonwebtoken/lang/Objects.java index eb475ac69..47cd81207 100644 --- a/src/main/java/io/jsonwebtoken/lang/Objects.java +++ b/src/main/java/io/jsonwebtoken/lang/Objects.java @@ -19,6 +19,7 @@ import java.io.IOException; import java.lang.reflect.Array; import java.util.Arrays; +import java.util.Collection; public final class Objects { @@ -95,6 +96,16 @@ public static boolean isEmpty(Object[] array) { return (array == null || array.length == 0); } + /** + * Determine whether the given collection is empty: + * i.e. null or of zero length. + * + * @param array the Collection to check + */ + public static boolean isEmpty(Collection collection) { + return (collection == null || collection.isEmpty()); + } + /** * Returns {@code true} if the specified byte array is null or of zero length, {@code false} otherwise. * From f1e1eecc25fefa5dd247b34bea7dd71f650276f4 Mon Sep 17 00:00:00 2001 From: Paul Cook Date: Tue, 25 Jul 2017 17:52:45 +0100 Subject: [PATCH 02/19] remove some <>'s and undo some accidental codestyle changes --- .../SigningKeyResolverAdapter.java | 4 +- .../jsonwebtoken/impl/DefaultJwtParser.java | 46 +++++++++---------- .../impl/crypto/MacValidator.java | 2 +- .../impl/crypto/RsaSignatureValidator.java | 2 +- 4 files changed, 27 insertions(+), 27 deletions(-) diff --git a/src/main/java/io/jsonwebtoken/SigningKeyResolverAdapter.java b/src/main/java/io/jsonwebtoken/SigningKeyResolverAdapter.java index dcedaf5d3..de9631f64 100644 --- a/src/main/java/io/jsonwebtoken/SigningKeyResolverAdapter.java +++ b/src/main/java/io/jsonwebtoken/SigningKeyResolverAdapter.java @@ -63,7 +63,7 @@ public Collection resolveSigningKeys(JwsHeader header, Claims claims) { Collection keysBytes = resolveSigningKeysBytes(header, claims); if (keysBytes == null) return null; - Collection keys = new ArrayList<>(); + Collection keys = new ArrayList(); for (byte[] keyBytes: keysBytes) keys.add(new SecretKeySpec(keyBytes, alg.getJcaName())); @@ -91,7 +91,7 @@ public Collection resolveSigningKeys(JwsHeader header, String plaintext) { Collection keysBytes = resolveSigningKeysBytes(header, plaintext); if (keysBytes == null) return null; - Collection keys = new ArrayList<>(); + Collection keys = new ArrayList(); for (byte[] keyBytes: keysBytes) keys.add(new SecretKeySpec(keyBytes, alg.getJcaName())); diff --git a/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java b/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java index 4cc81d7d2..40b5aaa32 100644 --- a/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java +++ b/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java @@ -61,7 +61,7 @@ public class DefaultJwtParser implements JwtParser { private static final String ISO_8601_FORMAT = "yyyy-MM-dd'T'HH:mm:ss'Z'"; private static final int MILLISECONDS_PER_SECOND = 1000; - private final ObjectMapper objectMapper = new ObjectMapper(); + private ObjectMapper objectMapper = new ObjectMapper(); private Collection keyBytes; @@ -79,43 +79,43 @@ public class DefaultJwtParser implements JwtParser { @Override public JwtParser requireIssuedAt(Date issuedAt) { - this.expectedClaims.setIssuedAt(issuedAt); + expectedClaims.setIssuedAt(issuedAt); return this; } @Override public JwtParser requireIssuer(String issuer) { - this.expectedClaims.setIssuer(issuer); + expectedClaims.setIssuer(issuer); return this; } @Override public JwtParser requireAudience(String audience) { - this.expectedClaims.setAudience(audience); + expectedClaims.setAudience(audience); return this; } @Override public JwtParser requireSubject(String subject) { - this.expectedClaims.setSubject(subject); + expectedClaims.setSubject(subject); return this; } @Override public JwtParser requireId(String id) { - this.expectedClaims.setId(id); + expectedClaims.setId(id); return this; } @Override public JwtParser requireExpiration(Date expiration) { - this.expectedClaims.setExpiration(expiration); + expectedClaims.setExpiration(expiration); return this; } @Override public JwtParser requireNotBefore(Date notBefore) { - this.expectedClaims.setNotBefore(notBefore); + expectedClaims.setNotBefore(notBefore); return this; } @@ -123,7 +123,7 @@ public JwtParser requireNotBefore(Date notBefore) { public JwtParser require(String claimName, Object value) { Assert.hasText(claimName, "claim name cannot be null or empty."); Assert.notNull(value, "The value cannot be null for claim name: " + claimName); - this.expectedClaims.put(claimName, value); + expectedClaims.put(claimName, value); return this; } @@ -144,7 +144,7 @@ public JwtParser setAllowedClockSkewSeconds(long seconds) { public JwtParser setSigningKey(byte[] key) { Assert.notEmpty(key, "signing key cannot be null or empty."); if (this.keyBytes == null) - this.keyBytes = new ArrayList<>(); + this.keyBytes = new ArrayList(); this.keyBytes.add(key); return this; } @@ -153,7 +153,7 @@ public JwtParser setSigningKey(byte[] key) { public JwtParser setSigningKey(String base64EncodedKeyBytes) { Assert.hasText(base64EncodedKeyBytes, "signing key cannot be null or empty."); if (this.keyBytes == null) - this.keyBytes = new ArrayList<>(); + this.keyBytes = new ArrayList(); this.keyBytes.add(TextCodec.BASE64.decode(base64EncodedKeyBytes)); return this; } @@ -162,7 +162,7 @@ public JwtParser setSigningKey(String base64EncodedKeyBytes) { public JwtParser setSigningKey(Key key) { Assert.notNull(key, "signing key cannot be null."); if (this.keys == null) - this.keys = new ArrayList<>(); + this.keys = new ArrayList(); this.keys.add(key); return this; } @@ -265,7 +265,7 @@ public Jwt parse(String jwt) throws ExpiredJwtException, MalformedJwtException, header = new DefaultHeader(m); } - compressionCodec = this.compressionCodecResolver.resolveCompressionCodec(header); + compressionCodec = compressionCodecResolver.resolveCompressionCodec(header); } // =============== Body ================= @@ -318,7 +318,7 @@ public Jwt parse(String jwt) throws ExpiredJwtException, MalformedJwtException, if (keys == null) { //fall back to keyBytes if (Objects.isEmpty(this.keyBytes) && this.signingKeyResolver != null) { //use the signingKeyResolver - keys = new ArrayList<>(); + keys = new ArrayList(); if (claims != null) { Key key = this.signingKeyResolver.resolveSigningKey(jwsHeader, claims); if (key != null) @@ -341,7 +341,7 @@ public Jwt parse(String jwt) throws ExpiredJwtException, MalformedJwtException, Assert.isTrue(algorithm.isHmac(), "Key bytes can only be specified for HMAC signatures. Please specify a PublicKey or PrivateKey instance."); - keys = new ArrayList<>(); + keys = new ArrayList(); for (byte[] bytes: this.keyBytes) this.keys.add(new SecretKeySpec(bytes, algorithm.getJcaName())); } @@ -441,9 +441,9 @@ public Jwt parse(String jwt) throws ExpiredJwtException, MalformedJwtException, } private void validateExpectedClaims(Header header, Claims claims) { - for (String expectedClaimName : this.expectedClaims.keySet()) { + for (String expectedClaimName : expectedClaims.keySet()) { - Object expectedClaimValue = this.expectedClaims.get(expectedClaimName); + Object expectedClaimValue = expectedClaims.get(expectedClaimName); Object actualClaimValue = claims.get(expectedClaimName); if ( @@ -451,7 +451,7 @@ private void validateExpectedClaims(Header header, Claims claims) { Claims.EXPIRATION.equals(expectedClaimName) || Claims.NOT_BEFORE.equals(expectedClaimName) ) { - expectedClaimValue = this.expectedClaims.get(expectedClaimName, Date.class); + expectedClaimValue = expectedClaims.get(expectedClaimName, Date.class); actualClaimValue = claims.get(expectedClaimName, Date.class); } else if ( expectedClaimValue instanceof Date && @@ -504,16 +504,16 @@ public T parse(String compact, JwtHandler handler) Jws jws = (Jws) jwt; Object body = jws.getBody(); if (body instanceof Claims) { - return handler.onClaimsJws(jws); + return handler.onClaimsJws((Jws) jws); } else { - return handler.onPlaintextJws(jws); + return handler.onPlaintextJws((Jws) jws); } } else { Object body = jwt.getBody(); if (body instanceof Claims) { - return handler.onClaimsJwt(jwt); + return handler.onClaimsJwt((Jwt) jwt); } else { - return handler.onPlaintextJwt(jwt); + return handler.onPlaintextJwt((Jwt) jwt); } } } @@ -569,7 +569,7 @@ public Jws onClaimsJws(Jws jws) { @SuppressWarnings("unchecked") protected Map readValue(String val) { try { - return this.objectMapper.readValue(val, Map.class); + return objectMapper.readValue(val, Map.class); } catch (IOException e) { throw new MalformedJwtException("Unable to read JSON value: " + val, e); } diff --git a/src/main/java/io/jsonwebtoken/impl/crypto/MacValidator.java b/src/main/java/io/jsonwebtoken/impl/crypto/MacValidator.java index ee5e3a314..f04f44b4b 100644 --- a/src/main/java/io/jsonwebtoken/impl/crypto/MacValidator.java +++ b/src/main/java/io/jsonwebtoken/impl/crypto/MacValidator.java @@ -27,7 +27,7 @@ public class MacValidator implements SignatureValidator { private final Collection signers; public MacValidator(SignatureAlgorithm alg, Collection keys) { - Collection signers = new ArrayList<>(); + Collection signers = new ArrayList(); for (Key key: keys) signers.add(new MacSigner(alg, key)); this.signers = signers; diff --git a/src/main/java/io/jsonwebtoken/impl/crypto/RsaSignatureValidator.java b/src/main/java/io/jsonwebtoken/impl/crypto/RsaSignatureValidator.java index f29f9082e..379d846a5 100644 --- a/src/main/java/io/jsonwebtoken/impl/crypto/RsaSignatureValidator.java +++ b/src/main/java/io/jsonwebtoken/impl/crypto/RsaSignatureValidator.java @@ -47,7 +47,7 @@ public SignerAndKey(final RsaSigner signer, final Key key) { public RsaSignatureValidator(SignatureAlgorithm alg, Collection keys) { super(alg, null); - Collection SIGNERS = new ArrayList<>(); + Collection SIGNERS = new ArrayList(); for (Key key: keys) { Assert.isTrue(key instanceof RSAPrivateKey || key instanceof RSAPublicKey, "RSA Signature validation requires either a RSAPublicKey or RSAPrivateKey instance."); From 75bd8a4b096e2707c8d4fac7e8a2237916e3f5d2 Mon Sep 17 00:00:00 2001 From: Paul Cook Date: Wed, 26 Jul 2017 15:34:31 +0100 Subject: [PATCH 03/19] tests --- .../jsonwebtoken/impl/crypto/MacProvider.java | 37 +++++++++++++++++++ ...efaultSignatureValidatorFactoryTest.groovy | 2 +- 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/src/main/java/io/jsonwebtoken/impl/crypto/MacProvider.java b/src/main/java/io/jsonwebtoken/impl/crypto/MacProvider.java index 1ce280bb9..98bdd73f0 100644 --- a/src/main/java/io/jsonwebtoken/impl/crypto/MacProvider.java +++ b/src/main/java/io/jsonwebtoken/impl/crypto/MacProvider.java @@ -22,6 +22,8 @@ import javax.crypto.spec.SecretKeySpec; import java.security.Key; import java.security.SecureRandom; +import java.util.ArrayList; +import java.util.Collection; public abstract class MacProvider extends SignatureProvider { @@ -44,6 +46,20 @@ public static SecretKey generateKey() { return generateKey(SignatureAlgorithm.HS512); } + /** + * Generates a collection of new secure-random 512 bit secret key suitable for creating and verifying HMAC signatures. This is a + * convenience method that immediately delegates to {@link #generateKey(SignatureAlgorithm)} using {@link + * SignatureAlgorithm#HS512} as the method argument. + * + * @return a new secure-random 512 bit secret key suitable for creating and verifying HMAC signatures. + * @see #generateKey(SignatureAlgorithm) + * @see #generateKey(SignatureAlgorithm, SecureRandom) + * @since 0.5 + */ + public static Collection generateKeys(int howMany) { + return generateKeys(SignatureAlgorithm.HS512, howMany); + } + /** * Generates a new secure-random secret key of a length suitable for creating and verifying HMAC signatures * according to the specified {@code SignatureAlgorithm} using JJWT's default {@link @@ -62,6 +78,27 @@ public static SecretKey generateKey(SignatureAlgorithm alg) { return generateKey(alg, SignatureProvider.DEFAULT_SECURE_RANDOM); } + /** + * Generates a new secure-random secret key of a length suitable for creating and verifying HMAC signatures + * according to the specified {@code SignatureAlgorithm} using JJWT's default {@link + * SignatureProvider#DEFAULT_SECURE_RANDOM SecureRandom instance}. This is a convenience method that immediately + * delegates to {@link #generateKey(SignatureAlgorithm, SecureRandom)}. + * + * @param alg the desired signature algorithm + * @return a new secure-random secret key of a length suitable for creating and verifying HMAC signatures according + * to the specified {@code SignatureAlgorithm} using JJWT's default {@link SignatureProvider#DEFAULT_SECURE_RANDOM + * SecureRandom instance}. + * @see #generateKey() + * @see #generateKey(SignatureAlgorithm, SecureRandom) + * @since 0.5 + */ + public static Collection generateKeys(SignatureAlgorithm alg, int howMany) { + Collection keys = new ArrayList(); + for (int i=0;i Date: Wed, 26 Jul 2017 15:52:01 +0100 Subject: [PATCH 04/19] Revert "remove some <>'s and undo some accidental codestyle changes" This reverts commit f1e1eecc25fefa5dd247b34bea7dd71f650276f4. --- .../SigningKeyResolverAdapter.java | 4 +- .../jsonwebtoken/impl/DefaultJwtParser.java | 46 +++++++++---------- .../impl/crypto/MacValidator.java | 2 +- .../impl/crypto/RsaSignatureValidator.java | 2 +- 4 files changed, 27 insertions(+), 27 deletions(-) diff --git a/src/main/java/io/jsonwebtoken/SigningKeyResolverAdapter.java b/src/main/java/io/jsonwebtoken/SigningKeyResolverAdapter.java index de9631f64..dcedaf5d3 100644 --- a/src/main/java/io/jsonwebtoken/SigningKeyResolverAdapter.java +++ b/src/main/java/io/jsonwebtoken/SigningKeyResolverAdapter.java @@ -63,7 +63,7 @@ public Collection resolveSigningKeys(JwsHeader header, Claims claims) { Collection keysBytes = resolveSigningKeysBytes(header, claims); if (keysBytes == null) return null; - Collection keys = new ArrayList(); + Collection keys = new ArrayList<>(); for (byte[] keyBytes: keysBytes) keys.add(new SecretKeySpec(keyBytes, alg.getJcaName())); @@ -91,7 +91,7 @@ public Collection resolveSigningKeys(JwsHeader header, String plaintext) { Collection keysBytes = resolveSigningKeysBytes(header, plaintext); if (keysBytes == null) return null; - Collection keys = new ArrayList(); + Collection keys = new ArrayList<>(); for (byte[] keyBytes: keysBytes) keys.add(new SecretKeySpec(keyBytes, alg.getJcaName())); diff --git a/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java b/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java index 40b5aaa32..4cc81d7d2 100644 --- a/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java +++ b/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java @@ -61,7 +61,7 @@ public class DefaultJwtParser implements JwtParser { private static final String ISO_8601_FORMAT = "yyyy-MM-dd'T'HH:mm:ss'Z'"; private static final int MILLISECONDS_PER_SECOND = 1000; - private ObjectMapper objectMapper = new ObjectMapper(); + private final ObjectMapper objectMapper = new ObjectMapper(); private Collection keyBytes; @@ -79,43 +79,43 @@ public class DefaultJwtParser implements JwtParser { @Override public JwtParser requireIssuedAt(Date issuedAt) { - expectedClaims.setIssuedAt(issuedAt); + this.expectedClaims.setIssuedAt(issuedAt); return this; } @Override public JwtParser requireIssuer(String issuer) { - expectedClaims.setIssuer(issuer); + this.expectedClaims.setIssuer(issuer); return this; } @Override public JwtParser requireAudience(String audience) { - expectedClaims.setAudience(audience); + this.expectedClaims.setAudience(audience); return this; } @Override public JwtParser requireSubject(String subject) { - expectedClaims.setSubject(subject); + this.expectedClaims.setSubject(subject); return this; } @Override public JwtParser requireId(String id) { - expectedClaims.setId(id); + this.expectedClaims.setId(id); return this; } @Override public JwtParser requireExpiration(Date expiration) { - expectedClaims.setExpiration(expiration); + this.expectedClaims.setExpiration(expiration); return this; } @Override public JwtParser requireNotBefore(Date notBefore) { - expectedClaims.setNotBefore(notBefore); + this.expectedClaims.setNotBefore(notBefore); return this; } @@ -123,7 +123,7 @@ public JwtParser requireNotBefore(Date notBefore) { public JwtParser require(String claimName, Object value) { Assert.hasText(claimName, "claim name cannot be null or empty."); Assert.notNull(value, "The value cannot be null for claim name: " + claimName); - expectedClaims.put(claimName, value); + this.expectedClaims.put(claimName, value); return this; } @@ -144,7 +144,7 @@ public JwtParser setAllowedClockSkewSeconds(long seconds) { public JwtParser setSigningKey(byte[] key) { Assert.notEmpty(key, "signing key cannot be null or empty."); if (this.keyBytes == null) - this.keyBytes = new ArrayList(); + this.keyBytes = new ArrayList<>(); this.keyBytes.add(key); return this; } @@ -153,7 +153,7 @@ public JwtParser setSigningKey(byte[] key) { public JwtParser setSigningKey(String base64EncodedKeyBytes) { Assert.hasText(base64EncodedKeyBytes, "signing key cannot be null or empty."); if (this.keyBytes == null) - this.keyBytes = new ArrayList(); + this.keyBytes = new ArrayList<>(); this.keyBytes.add(TextCodec.BASE64.decode(base64EncodedKeyBytes)); return this; } @@ -162,7 +162,7 @@ public JwtParser setSigningKey(String base64EncodedKeyBytes) { public JwtParser setSigningKey(Key key) { Assert.notNull(key, "signing key cannot be null."); if (this.keys == null) - this.keys = new ArrayList(); + this.keys = new ArrayList<>(); this.keys.add(key); return this; } @@ -265,7 +265,7 @@ public Jwt parse(String jwt) throws ExpiredJwtException, MalformedJwtException, header = new DefaultHeader(m); } - compressionCodec = compressionCodecResolver.resolveCompressionCodec(header); + compressionCodec = this.compressionCodecResolver.resolveCompressionCodec(header); } // =============== Body ================= @@ -318,7 +318,7 @@ public Jwt parse(String jwt) throws ExpiredJwtException, MalformedJwtException, if (keys == null) { //fall back to keyBytes if (Objects.isEmpty(this.keyBytes) && this.signingKeyResolver != null) { //use the signingKeyResolver - keys = new ArrayList(); + keys = new ArrayList<>(); if (claims != null) { Key key = this.signingKeyResolver.resolveSigningKey(jwsHeader, claims); if (key != null) @@ -341,7 +341,7 @@ public Jwt parse(String jwt) throws ExpiredJwtException, MalformedJwtException, Assert.isTrue(algorithm.isHmac(), "Key bytes can only be specified for HMAC signatures. Please specify a PublicKey or PrivateKey instance."); - keys = new ArrayList(); + keys = new ArrayList<>(); for (byte[] bytes: this.keyBytes) this.keys.add(new SecretKeySpec(bytes, algorithm.getJcaName())); } @@ -441,9 +441,9 @@ public Jwt parse(String jwt) throws ExpiredJwtException, MalformedJwtException, } private void validateExpectedClaims(Header header, Claims claims) { - for (String expectedClaimName : expectedClaims.keySet()) { + for (String expectedClaimName : this.expectedClaims.keySet()) { - Object expectedClaimValue = expectedClaims.get(expectedClaimName); + Object expectedClaimValue = this.expectedClaims.get(expectedClaimName); Object actualClaimValue = claims.get(expectedClaimName); if ( @@ -451,7 +451,7 @@ private void validateExpectedClaims(Header header, Claims claims) { Claims.EXPIRATION.equals(expectedClaimName) || Claims.NOT_BEFORE.equals(expectedClaimName) ) { - expectedClaimValue = expectedClaims.get(expectedClaimName, Date.class); + expectedClaimValue = this.expectedClaims.get(expectedClaimName, Date.class); actualClaimValue = claims.get(expectedClaimName, Date.class); } else if ( expectedClaimValue instanceof Date && @@ -504,16 +504,16 @@ public T parse(String compact, JwtHandler handler) Jws jws = (Jws) jwt; Object body = jws.getBody(); if (body instanceof Claims) { - return handler.onClaimsJws((Jws) jws); + return handler.onClaimsJws(jws); } else { - return handler.onPlaintextJws((Jws) jws); + return handler.onPlaintextJws(jws); } } else { Object body = jwt.getBody(); if (body instanceof Claims) { - return handler.onClaimsJwt((Jwt) jwt); + return handler.onClaimsJwt(jwt); } else { - return handler.onPlaintextJwt((Jwt) jwt); + return handler.onPlaintextJwt(jwt); } } } @@ -569,7 +569,7 @@ public Jws onClaimsJws(Jws jws) { @SuppressWarnings("unchecked") protected Map readValue(String val) { try { - return objectMapper.readValue(val, Map.class); + return this.objectMapper.readValue(val, Map.class); } catch (IOException e) { throw new MalformedJwtException("Unable to read JSON value: " + val, e); } diff --git a/src/main/java/io/jsonwebtoken/impl/crypto/MacValidator.java b/src/main/java/io/jsonwebtoken/impl/crypto/MacValidator.java index f04f44b4b..ee5e3a314 100644 --- a/src/main/java/io/jsonwebtoken/impl/crypto/MacValidator.java +++ b/src/main/java/io/jsonwebtoken/impl/crypto/MacValidator.java @@ -27,7 +27,7 @@ public class MacValidator implements SignatureValidator { private final Collection signers; public MacValidator(SignatureAlgorithm alg, Collection keys) { - Collection signers = new ArrayList(); + Collection signers = new ArrayList<>(); for (Key key: keys) signers.add(new MacSigner(alg, key)); this.signers = signers; diff --git a/src/main/java/io/jsonwebtoken/impl/crypto/RsaSignatureValidator.java b/src/main/java/io/jsonwebtoken/impl/crypto/RsaSignatureValidator.java index 379d846a5..f29f9082e 100644 --- a/src/main/java/io/jsonwebtoken/impl/crypto/RsaSignatureValidator.java +++ b/src/main/java/io/jsonwebtoken/impl/crypto/RsaSignatureValidator.java @@ -47,7 +47,7 @@ public SignerAndKey(final RsaSigner signer, final Key key) { public RsaSignatureValidator(SignatureAlgorithm alg, Collection keys) { super(alg, null); - Collection SIGNERS = new ArrayList(); + Collection SIGNERS = new ArrayList<>(); for (Key key: keys) { Assert.isTrue(key instanceof RSAPrivateKey || key instanceof RSAPublicKey, "RSA Signature validation requires either a RSAPublicKey or RSAPrivateKey instance."); From 3374298ffe217043511d57be680e4b6f1502072c Mon Sep 17 00:00:00 2001 From: Paul Cook Date: Wed, 26 Jul 2017 15:52:26 +0100 Subject: [PATCH 05/19] Revert "add support for supplying multiple public keys that will be attempted when validating a token signature" This reverts commit ba3ae16141d887a7a4f8d067384ad84126a7936b. --- .../io/jsonwebtoken/SigningKeyResolver.java | 24 ----- .../SigningKeyResolverAdapter.java | 71 -------------- .../jsonwebtoken/impl/DefaultJwtParser.java | 98 ++++++++----------- .../crypto/DefaultJwtSignatureValidator.java | 9 +- .../DefaultSignatureValidatorFactory.java | 11 +-- .../EllipticCurveSignatureValidator.java | 45 ++++----- .../impl/crypto/MacValidator.java | 20 ++-- .../impl/crypto/RsaSignatureValidator.java | 59 ++++------- .../crypto/SignatureValidatorFactory.java | 3 +- .../java/io/jsonwebtoken/lang/Objects.java | 11 --- 10 files changed, 91 insertions(+), 260 deletions(-) diff --git a/src/main/java/io/jsonwebtoken/SigningKeyResolver.java b/src/main/java/io/jsonwebtoken/SigningKeyResolver.java index 789a9c2c8..b068db9d2 100644 --- a/src/main/java/io/jsonwebtoken/SigningKeyResolver.java +++ b/src/main/java/io/jsonwebtoken/SigningKeyResolver.java @@ -16,7 +16,6 @@ package io.jsonwebtoken; import java.security.Key; -import java.util.Collection; /** * A {@code SigningKeyResolver} can be used by a {@link io.jsonwebtoken.JwtParser JwtParser} to find a signing key that @@ -61,17 +60,6 @@ public interface SigningKeyResolver { */ Key resolveSigningKey(JwsHeader header, Claims claims); - /** - * Returns a collection signing key that should be used to attempt to validate a digital signature for the Claims JWS with the specified - * header and claims. This allows for a key rotation scenario to support multiple keys during an overlap period. - * - * @param header the header of the JWS to validate - * @param claims the claims (body) of the JWS to validate - * @return the signing key that should be used to validate a digital signature for the Claims JWS with the specified - * header and claims. - */ - Collection resolveSigningKeys(JwsHeader header, Claims claims); - /** * Returns the signing key that should be used to validate a digital signature for the Plaintext JWS with the * specified header and plaintext payload. @@ -82,16 +70,4 @@ public interface SigningKeyResolver { * specified header and plaintext payload. */ Key resolveSigningKey(JwsHeader header, String plaintext); - - /** - * Returns the signing key that should be used to attempt to validate a digital signature for the Plaintext JWS with the - * specified header and plaintext payload. This allows for a key rotation scenario to support multiple keys during an overlap period. - * - * @param header the header of the JWS to validate - * @param plaintext the plaintext body of the JWS to validate - * @return the signing key that should be used to validate a digital signature for the Plaintext JWS with the - * specified header and plaintext payload. - */ - Collection resolveSigningKeys(JwsHeader header, String plaintext); - } diff --git a/src/main/java/io/jsonwebtoken/SigningKeyResolverAdapter.java b/src/main/java/io/jsonwebtoken/SigningKeyResolverAdapter.java index dcedaf5d3..1be7ec556 100644 --- a/src/main/java/io/jsonwebtoken/SigningKeyResolverAdapter.java +++ b/src/main/java/io/jsonwebtoken/SigningKeyResolverAdapter.java @@ -19,8 +19,6 @@ import javax.crypto.spec.SecretKeySpec; import java.security.Key; -import java.util.ArrayList; -import java.util.Collection; /** * An Adapter implementation of the @@ -53,23 +51,6 @@ public Key resolveSigningKey(JwsHeader header, Claims claims) { return new SecretKeySpec(keyBytes, alg.getJcaName()); } - @Override - public Collection resolveSigningKeys(JwsHeader header, Claims claims) { - SignatureAlgorithm alg = SignatureAlgorithm.forName(header.getAlgorithm()); - Assert.isTrue(alg.isHmac(), "The default resolveSigningKey(JwsHeader, Claims) implementation cannot be " + - "used for asymmetric key algorithms (RSA, Elliptic Curve). " + - "Override the resolveSigningKey(JwsHeader, Claims) method instead and return a " + - "Key instance appropriate for the " + alg.name() + " algorithm."); - Collection keysBytes = resolveSigningKeysBytes(header, claims); - if (keysBytes == null) - return null; - Collection keys = new ArrayList<>(); - for (byte[] keyBytes: keysBytes) - keys.add(new SecretKeySpec(keyBytes, alg.getJcaName())); - - return keys; - } - @Override public Key resolveSigningKey(JwsHeader header, String plaintext) { SignatureAlgorithm alg = SignatureAlgorithm.forName(header.getAlgorithm()); @@ -81,23 +62,6 @@ public Key resolveSigningKey(JwsHeader header, String plaintext) { return new SecretKeySpec(keyBytes, alg.getJcaName()); } - @Override - public Collection resolveSigningKeys(JwsHeader header, String plaintext) { - SignatureAlgorithm alg = SignatureAlgorithm.forName(header.getAlgorithm()); - Assert.isTrue(alg.isHmac(), "The default resolveSigningKey(JwsHeader, String) implementation cannot be " + - "used for asymmetric key algorithms (RSA, Elliptic Curve). " + - "Override the resolveSigningKey(JwsHeader, String) method instead and return a " + - "Key instance appropriate for the " + alg.name() + " algorithm."); - Collection keysBytes = resolveSigningKeysBytes(header, plaintext); - if (keysBytes == null) - return null; - Collection keys = new ArrayList<>(); - for (byte[] keyBytes: keysBytes) - keys.add(new SecretKeySpec(keyBytes, alg.getJcaName())); - - return keys; - } - /** * Convenience method invoked by {@link #resolveSigningKey(JwsHeader, Claims)} that obtains the necessary signing * key bytes. This implementation simply throws an exception: if the JWS parsed is a Claims JWS, you must @@ -117,25 +81,6 @@ public byte[] resolveSigningKeyBytes(JwsHeader header, Claims claims) { "resolveSigningKeyBytes(JwsHeader, Claims) method."); } - /** - * Convenience method invoked by {@link #resolveSigningKey(JwsHeader, Claims)} that obtains the necessary signing - * key bytes. This implementation simply throws an exception: if the JWS parsed is a Claims JWS, you must - * override this method or the {@link #resolveSigningKey(JwsHeader, Claims)} method instead. - * - *

NOTE: You cannot override this method when validating RSA signatures. If you expect RSA signatures, - * you must override the {@link #resolveSigningKey(JwsHeader, Claims)} method instead.

- * - * @param header the parsed {@link JwsHeader} - * @param claims the parsed {@link Claims} - * @return the signing key bytes to use to verify the JWS signature. - */ - public Collection resolveSigningKeysBytes(JwsHeader header, Claims claims) { - throw new UnsupportedJwtException("The specified SigningKeyResolver implementation does not support " + - "Claims JWS signing key resolution. Consider overriding either the " + - "resolveSigningKey(JwsHeader, Claims) method or, for HMAC algorithms, the " + - "resolveSigningKeyBytes(JwsHeader, Claims) method."); - } - /** * Convenience method invoked by {@link #resolveSigningKey(JwsHeader, String)} that obtains the necessary signing * key bytes. This implementation simply throws an exception: if the JWS parsed is a plaintext JWS, you must @@ -151,20 +96,4 @@ public byte[] resolveSigningKeyBytes(JwsHeader header, String payload) { "resolveSigningKey(JwsHeader, String) method or, for HMAC algorithms, the " + "resolveSigningKeyBytes(JwsHeader, String) method."); } - - /** - * Convenience method invoked by {@link #resolveSigningKey(JwsHeader, String)} that obtains the necessary signing - * key bytes. This implementation simply throws an exception: if the JWS parsed is a plaintext JWS, you must - * override this method or the {@link #resolveSigningKey(JwsHeader, String)} method instead. - * - * @param header the parsed {@link JwsHeader} - * @param payload the parsed String plaintext payload - * @return the signing key bytes to use to verify the JWS signature. - */ - public Collection resolveSigningKeysBytes(JwsHeader header, String payload) { - throw new UnsupportedJwtException("The specified SigningKeyResolver implementation does not support " + - "plaintext JWS signing key resolution. Consider overriding either the " + - "resolveSigningKey(JwsHeader, String) method or, for HMAC algorithms, the " + - "resolveSigningKeyBytes(JwsHeader, String) method."); - } } diff --git a/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java b/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java index 4cc81d7d2..4e4b9c79c 100644 --- a/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java +++ b/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java @@ -49,8 +49,6 @@ import java.io.IOException; import java.security.Key; import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.Collection; import java.util.Date; import java.util.Map; @@ -61,11 +59,11 @@ public class DefaultJwtParser implements JwtParser { private static final String ISO_8601_FORMAT = "yyyy-MM-dd'T'HH:mm:ss'Z'"; private static final int MILLISECONDS_PER_SECOND = 1000; - private final ObjectMapper objectMapper = new ObjectMapper(); + private ObjectMapper objectMapper = new ObjectMapper(); - private Collection keyBytes; + private byte[] keyBytes; - private Collection keys; + private Key key; private SigningKeyResolver signingKeyResolver; @@ -79,43 +77,43 @@ public class DefaultJwtParser implements JwtParser { @Override public JwtParser requireIssuedAt(Date issuedAt) { - this.expectedClaims.setIssuedAt(issuedAt); + expectedClaims.setIssuedAt(issuedAt); return this; } @Override public JwtParser requireIssuer(String issuer) { - this.expectedClaims.setIssuer(issuer); + expectedClaims.setIssuer(issuer); return this; } @Override public JwtParser requireAudience(String audience) { - this.expectedClaims.setAudience(audience); + expectedClaims.setAudience(audience); return this; } @Override public JwtParser requireSubject(String subject) { - this.expectedClaims.setSubject(subject); + expectedClaims.setSubject(subject); return this; } @Override public JwtParser requireId(String id) { - this.expectedClaims.setId(id); + expectedClaims.setId(id); return this; } @Override public JwtParser requireExpiration(Date expiration) { - this.expectedClaims.setExpiration(expiration); + expectedClaims.setExpiration(expiration); return this; } @Override public JwtParser requireNotBefore(Date notBefore) { - this.expectedClaims.setNotBefore(notBefore); + expectedClaims.setNotBefore(notBefore); return this; } @@ -123,7 +121,7 @@ public JwtParser requireNotBefore(Date notBefore) { public JwtParser require(String claimName, Object value) { Assert.hasText(claimName, "claim name cannot be null or empty."); Assert.notNull(value, "The value cannot be null for claim name: " + claimName); - this.expectedClaims.put(claimName, value); + expectedClaims.put(claimName, value); return this; } @@ -143,27 +141,21 @@ public JwtParser setAllowedClockSkewSeconds(long seconds) { @Override public JwtParser setSigningKey(byte[] key) { Assert.notEmpty(key, "signing key cannot be null or empty."); - if (this.keyBytes == null) - this.keyBytes = new ArrayList<>(); - this.keyBytes.add(key); + this.keyBytes = key; return this; } @Override public JwtParser setSigningKey(String base64EncodedKeyBytes) { Assert.hasText(base64EncodedKeyBytes, "signing key cannot be null or empty."); - if (this.keyBytes == null) - this.keyBytes = new ArrayList<>(); - this.keyBytes.add(TextCodec.BASE64.decode(base64EncodedKeyBytes)); + this.keyBytes = TextCodec.BASE64.decode(base64EncodedKeyBytes); return this; } @Override public JwtParser setSigningKey(Key key) { Assert.notNull(key, "signing key cannot be null."); - if (this.keys == null) - this.keys = new ArrayList<>(); - this.keys.add(key); + this.key = key; return this; } @@ -265,7 +257,7 @@ public Jwt parse(String jwt) throws ExpiredJwtException, MalformedJwtException, header = new DefaultHeader(m); } - compressionCodec = this.compressionCodecResolver.resolveCompressionCodec(header); + compressionCodec = compressionCodecResolver.resolveCompressionCodec(header); } // =============== Body ================= @@ -305,59 +297,47 @@ public Jwt parse(String jwt) throws ExpiredJwtException, MalformedJwtException, throw new MalformedJwtException(msg); } - if (this.keys != null && this.keyBytes != null) { + if (key != null && keyBytes != null) { throw new IllegalStateException("A key object and key bytes cannot both be specified. Choose either."); - } else if ((this.keys != null || this.keyBytes != null) && this.signingKeyResolver != null) { - String object = this.keys != null ? "a key object" : "key bytes"; + } else if ((key != null || keyBytes != null) && signingKeyResolver != null) { + String object = key != null ? "a key object" : "key bytes"; throw new IllegalStateException("A signing key resolver and " + object + " cannot both be specified. Choose either."); } //digitally signed, let's assert the signature: - Collection keys = this.keys; + Key key = this.key; - if (keys == null) { //fall back to keyBytes + if (key == null) { //fall back to keyBytes - if (Objects.isEmpty(this.keyBytes) && this.signingKeyResolver != null) { //use the signingKeyResolver - keys = new ArrayList<>(); + byte[] keyBytes = this.keyBytes; + + if (Objects.isEmpty(keyBytes) && signingKeyResolver != null) { //use the signingKeyResolver if (claims != null) { - Key key = this.signingKeyResolver.resolveSigningKey(jwsHeader, claims); - if (key != null) - keys.add(key); - Collection keyList = this.signingKeyResolver.resolveSigningKeys(jwsHeader, claims); - if (!Objects.isEmpty(keyList)) - keys.addAll(keyList); + key = signingKeyResolver.resolveSigningKey(jwsHeader, claims); } else { - Key key = this.signingKeyResolver.resolveSigningKey(jwsHeader, payload); - if (key != null) - keys.add(key); - Collection keyList = this.signingKeyResolver.resolveSigningKeys(jwsHeader, payload); - if (!Objects.isEmpty(keyList)) - keys.addAll(keyList); + key = signingKeyResolver.resolveSigningKey(jwsHeader, payload); } } - if (!Objects.isEmpty(this.keyBytes)) { + if (!Objects.isEmpty(keyBytes)) { Assert.isTrue(algorithm.isHmac(), "Key bytes can only be specified for HMAC signatures. Please specify a PublicKey or PrivateKey instance."); - keys = new ArrayList<>(); - for (byte[] bytes: this.keyBytes) - this.keys.add(new SecretKeySpec(bytes, algorithm.getJcaName())); + key = new SecretKeySpec(keyBytes, algorithm.getJcaName()); } } - Assert.notNull(keys, "A signing key must be specified if the specified JWT is digitally signed."); + Assert.notNull(key, "A signing key must be specified if the specified JWT is digitally signed."); //re-create the jwt part without the signature. This is what needs to be signed for verification: String jwtWithoutSignature = base64UrlEncodedHeader + SEPARATOR_CHAR + base64UrlEncodedPayload; JwtSignatureValidator validator; try { - validator = createSignatureValidator(algorithm, keys); + validator = createSignatureValidator(algorithm, key); } catch (IllegalArgumentException e) { String algName = algorithm.getValue(); - Key key = keys.iterator().next(); String msg = "The parsed JWT indicates it was signed with the " + algName + " signature " + "algorithm, but the specified signing key of type " + key.getClass().getName() + " may not be used to validate " + algName + " signatures. Because the specified " + @@ -441,9 +421,9 @@ public Jwt parse(String jwt) throws ExpiredJwtException, MalformedJwtException, } private void validateExpectedClaims(Header header, Claims claims) { - for (String expectedClaimName : this.expectedClaims.keySet()) { + for (String expectedClaimName : expectedClaims.keySet()) { - Object expectedClaimValue = this.expectedClaims.get(expectedClaimName); + Object expectedClaimValue = expectedClaims.get(expectedClaimName); Object actualClaimValue = claims.get(expectedClaimName); if ( @@ -451,7 +431,7 @@ private void validateExpectedClaims(Header header, Claims claims) { Claims.EXPIRATION.equals(expectedClaimName) || Claims.NOT_BEFORE.equals(expectedClaimName) ) { - expectedClaimValue = this.expectedClaims.get(expectedClaimName, Date.class); + expectedClaimValue = expectedClaims.get(expectedClaimName, Date.class); actualClaimValue = claims.get(expectedClaimName, Date.class); } else if ( expectedClaimValue instanceof Date && @@ -488,8 +468,8 @@ private void validateExpectedClaims(Header header, Claims claims) { /* * @since 0.5 mostly to allow testing overrides */ - protected JwtSignatureValidator createSignatureValidator(SignatureAlgorithm alg, Collection keys) { - return new DefaultJwtSignatureValidator(alg, keys); + protected JwtSignatureValidator createSignatureValidator(SignatureAlgorithm alg, Key key) { + return new DefaultJwtSignatureValidator(alg, key); } @Override @@ -504,16 +484,16 @@ public T parse(String compact, JwtHandler handler) Jws jws = (Jws) jwt; Object body = jws.getBody(); if (body instanceof Claims) { - return handler.onClaimsJws(jws); + return handler.onClaimsJws((Jws) jws); } else { - return handler.onPlaintextJws(jws); + return handler.onPlaintextJws((Jws) jws); } } else { Object body = jwt.getBody(); if (body instanceof Claims) { - return handler.onClaimsJwt(jwt); + return handler.onClaimsJwt((Jwt) jwt); } else { - return handler.onPlaintextJwt(jwt); + return handler.onPlaintextJwt((Jwt) jwt); } } } @@ -569,7 +549,7 @@ public Jws onClaimsJws(Jws jws) { @SuppressWarnings("unchecked") protected Map readValue(String val) { try { - return this.objectMapper.readValue(val, Map.class); + return objectMapper.readValue(val, Map.class); } catch (IOException e) { throw new MalformedJwtException("Unable to read JSON value: " + val, e); } diff --git a/src/main/java/io/jsonwebtoken/impl/crypto/DefaultJwtSignatureValidator.java b/src/main/java/io/jsonwebtoken/impl/crypto/DefaultJwtSignatureValidator.java index 0f48a5d32..245c0d551 100644 --- a/src/main/java/io/jsonwebtoken/impl/crypto/DefaultJwtSignatureValidator.java +++ b/src/main/java/io/jsonwebtoken/impl/crypto/DefaultJwtSignatureValidator.java @@ -21,7 +21,6 @@ import java.nio.charset.Charset; import java.security.Key; -import java.util.Collection; public class DefaultJwtSignatureValidator implements JwtSignatureValidator { @@ -29,13 +28,13 @@ public class DefaultJwtSignatureValidator implements JwtSignatureValidator { private final SignatureValidator signatureValidator; - public DefaultJwtSignatureValidator(SignatureAlgorithm alg, Collection keys) { - this(DefaultSignatureValidatorFactory.INSTANCE, alg, keys); + public DefaultJwtSignatureValidator(SignatureAlgorithm alg, Key key) { + this(DefaultSignatureValidatorFactory.INSTANCE, alg, key); } - public DefaultJwtSignatureValidator(SignatureValidatorFactory factory, SignatureAlgorithm alg, Collection keys) { + public DefaultJwtSignatureValidator(SignatureValidatorFactory factory, SignatureAlgorithm alg, Key key) { Assert.notNull(factory, "SignerFactory argument cannot be null."); - this.signatureValidator = factory.createSignatureValidator(alg, keys); + this.signatureValidator = factory.createSignatureValidator(alg, key); } @Override diff --git a/src/main/java/io/jsonwebtoken/impl/crypto/DefaultSignatureValidatorFactory.java b/src/main/java/io/jsonwebtoken/impl/crypto/DefaultSignatureValidatorFactory.java index be909c6f5..82916847c 100644 --- a/src/main/java/io/jsonwebtoken/impl/crypto/DefaultSignatureValidatorFactory.java +++ b/src/main/java/io/jsonwebtoken/impl/crypto/DefaultSignatureValidatorFactory.java @@ -19,33 +19,32 @@ import io.jsonwebtoken.lang.Assert; import java.security.Key; -import java.util.Collection; public class DefaultSignatureValidatorFactory implements SignatureValidatorFactory { public static final SignatureValidatorFactory INSTANCE = new DefaultSignatureValidatorFactory(); @Override - public SignatureValidator createSignatureValidator(SignatureAlgorithm alg, Collection keys) { + public SignatureValidator createSignatureValidator(SignatureAlgorithm alg, Key key) { Assert.notNull(alg, "SignatureAlgorithm cannot be null."); - Assert.notNull(keys, "Signing Key cannot be null."); + Assert.notNull(key, "Signing Key cannot be null."); switch (alg) { case HS256: case HS384: case HS512: - return new MacValidator(alg, keys); + return new MacValidator(alg, key); case RS256: case RS384: case RS512: case PS256: case PS384: case PS512: - return new RsaSignatureValidator(alg, keys); + return new RsaSignatureValidator(alg, key); case ES256: case ES384: case ES512: - return new EllipticCurveSignatureValidator(alg, keys); + return new EllipticCurveSignatureValidator(alg, key); default: throw new IllegalArgumentException("The '" + alg.name() + "' algorithm cannot be used for signing."); } diff --git a/src/main/java/io/jsonwebtoken/impl/crypto/EllipticCurveSignatureValidator.java b/src/main/java/io/jsonwebtoken/impl/crypto/EllipticCurveSignatureValidator.java index ed548e37f..09ab14db6 100644 --- a/src/main/java/io/jsonwebtoken/impl/crypto/EllipticCurveSignatureValidator.java +++ b/src/main/java/io/jsonwebtoken/impl/crypto/EllipticCurveSignatureValidator.java @@ -20,7 +20,6 @@ import java.security.PublicKey; import java.security.Signature; import java.security.interfaces.ECPublicKey; -import java.util.Collection; import io.jsonwebtoken.SignatureAlgorithm; import io.jsonwebtoken.SignatureException; @@ -31,38 +30,30 @@ public class EllipticCurveSignatureValidator extends EllipticCurveProvider imple private static final String EC_PUBLIC_KEY_REQD_MSG = "Elliptic Curve signature validation requires an ECPublicKey instance."; - private final Collection keys; - - public EllipticCurveSignatureValidator(SignatureAlgorithm alg, Collection keys) { - super(alg, null); - this.keys = keys; - for (Key key: keys) - Assert.isTrue(key instanceof ECPublicKey, EC_PUBLIC_KEY_REQD_MSG); + public EllipticCurveSignatureValidator(SignatureAlgorithm alg, Key key) { + super(alg, key); + Assert.isTrue(key instanceof ECPublicKey, EC_PUBLIC_KEY_REQD_MSG); } @Override public boolean isValid(byte[] data, byte[] signature) { Signature sig = createSignatureInstance(); - for (Key key: this.keys) { - PublicKey publicKey = (PublicKey) key; - try { - int expectedSize = getSignatureByteArrayLength(this.alg); - /** - * - * If the expected size is not valid for JOSE, fall back to ASN.1 DER signature. - * This fallback is for backwards compatibility ONLY (to support tokens generated by previous versions of jjwt) - * and backwards compatibility will possibly be removed in a future version of this library. - * - * **/ - byte[] derSignature = expectedSize != signature.length && signature[0] == 0x30 ? signature : EllipticCurveProvider.transcodeSignatureToDER(signature); - if (doVerify(sig, publicKey, data, derSignature)) - return true; - } catch (Exception e) { - String msg = "Unable to verify Elliptic Curve signature using configured ECPublicKey. " + e.getMessage(); - throw new SignatureException(msg, e); - } + PublicKey publicKey = (PublicKey) key; + try { + int expectedSize = getSignatureByteArrayLength(alg); + /** + * + * If the expected size is not valid for JOSE, fall back to ASN.1 DER signature. + * This fallback is for backwards compatibility ONLY (to support tokens generated by previous versions of jjwt) + * and backwards compatibility will possibly be removed in a future version of this library. + * + * **/ + byte[] derSignature = expectedSize != signature.length && signature[0] == 0x30 ? signature : EllipticCurveProvider.transcodeSignatureToDER(signature); + return doVerify(sig, publicKey, data, derSignature); + } catch (Exception e) { + String msg = "Unable to verify Elliptic Curve signature using configured ECPublicKey. " + e.getMessage(); + throw new SignatureException(msg, e); } - return false; } protected boolean doVerify(Signature sig, PublicKey publicKey, byte[] data, byte[] signature) diff --git a/src/main/java/io/jsonwebtoken/impl/crypto/MacValidator.java b/src/main/java/io/jsonwebtoken/impl/crypto/MacValidator.java index ee5e3a314..af3196661 100644 --- a/src/main/java/io/jsonwebtoken/impl/crypto/MacValidator.java +++ b/src/main/java/io/jsonwebtoken/impl/crypto/MacValidator.java @@ -19,27 +19,19 @@ import java.security.Key; import java.security.MessageDigest; -import java.util.ArrayList; -import java.util.Collection; +import java.util.Arrays; public class MacValidator implements SignatureValidator { - private final Collection signers; + private final MacSigner signer; - public MacValidator(SignatureAlgorithm alg, Collection keys) { - Collection signers = new ArrayList<>(); - for (Key key: keys) - signers.add(new MacSigner(alg, key)); - this.signers = signers; + public MacValidator(SignatureAlgorithm alg, Key key) { + this.signer = new MacSigner(alg, key); } @Override public boolean isValid(byte[] data, byte[] signature) { - for (MacSigner signer: this.signers) { - byte[] computed = signer.sign(data); - if (MessageDigest.isEqual(computed, signature)) - return true; - } - return false; + byte[] computed = this.signer.sign(data); + return MessageDigest.isEqual(computed, signature); } } diff --git a/src/main/java/io/jsonwebtoken/impl/crypto/RsaSignatureValidator.java b/src/main/java/io/jsonwebtoken/impl/crypto/RsaSignatureValidator.java index f29f9082e..a306d748f 100644 --- a/src/main/java/io/jsonwebtoken/impl/crypto/RsaSignatureValidator.java +++ b/src/main/java/io/jsonwebtoken/impl/crypto/RsaSignatureValidator.java @@ -25,58 +25,35 @@ import java.security.Signature; import java.security.interfaces.RSAPrivateKey; import java.security.interfaces.RSAPublicKey; -import java.util.ArrayList; import java.util.Arrays; -import java.util.Collection; public class RsaSignatureValidator extends RsaProvider implements SignatureValidator { - private final Collection SIGNERS; + private final RsaSigner SIGNER; - public static final class SignerAndKey { - - private final RsaSigner signer; - private final Key key; - - public SignerAndKey(final RsaSigner signer, final Key key) { - this.signer = signer; - this.key = key; - } - } - - public RsaSignatureValidator(SignatureAlgorithm alg, Collection keys) { - super(alg, null); - - Collection SIGNERS = new ArrayList<>(); - for (Key key: keys) { - Assert.isTrue(key instanceof RSAPrivateKey || key instanceof RSAPublicKey, - "RSA Signature validation requires either a RSAPublicKey or RSAPrivateKey instance."); - SIGNERS.add(new SignerAndKey(new RsaSigner(alg, key), key)); - } - this.SIGNERS = SIGNERS; + public RsaSignatureValidator(SignatureAlgorithm alg, Key key) { + super(alg, key); + Assert.isTrue(key instanceof RSAPrivateKey || key instanceof RSAPublicKey, + "RSA Signature validation requires either a RSAPublicKey or RSAPrivateKey instance."); + this.SIGNER = key instanceof RSAPrivateKey ? new RsaSigner(alg, key) : null; } @Override public boolean isValid(byte[] data, byte[] signature) { - for (SignerAndKey signerAndKey: this.SIGNERS) { - if (signerAndKey.key instanceof PublicKey) { - Signature sig = createSignatureInstance(); - PublicKey publicKey = (PublicKey) signerAndKey.key; - try { - if (doVerify(sig, publicKey, data, signature)) - return true; - } catch (Exception e) { - String msg = "Unable to verify RSA signature using configured PublicKey. " + e.getMessage(); - throw new SignatureException(msg, e); - } - } else { - Assert.notNull(this.SIGNERS, "RSA Signer instance cannot be null. This is a bug. Please report it."); - byte[] computed = signerAndKey.signer.sign(data); - if (Arrays.equals(computed, signature)) - return true; + if (key instanceof PublicKey) { + Signature sig = createSignatureInstance(); + PublicKey publicKey = (PublicKey) key; + try { + return doVerify(sig, publicKey, data, signature); + } catch (Exception e) { + String msg = "Unable to verify RSA signature using configured PublicKey. " + e.getMessage(); + throw new SignatureException(msg, e); } + } else { + Assert.notNull(this.SIGNER, "RSA Signer instance cannot be null. This is a bug. Please report it."); + byte[] computed = this.SIGNER.sign(data); + return Arrays.equals(computed, signature); } - return false; } protected boolean doVerify(Signature sig, PublicKey publicKey, byte[] data, byte[] signature) diff --git a/src/main/java/io/jsonwebtoken/impl/crypto/SignatureValidatorFactory.java b/src/main/java/io/jsonwebtoken/impl/crypto/SignatureValidatorFactory.java index 7712f4a4d..1e84b620e 100644 --- a/src/main/java/io/jsonwebtoken/impl/crypto/SignatureValidatorFactory.java +++ b/src/main/java/io/jsonwebtoken/impl/crypto/SignatureValidatorFactory.java @@ -18,9 +18,8 @@ import io.jsonwebtoken.SignatureAlgorithm; import java.security.Key; -import java.util.Collection; public interface SignatureValidatorFactory { - SignatureValidator createSignatureValidator(SignatureAlgorithm alg, Collection keys); + SignatureValidator createSignatureValidator(SignatureAlgorithm alg, Key key); } diff --git a/src/main/java/io/jsonwebtoken/lang/Objects.java b/src/main/java/io/jsonwebtoken/lang/Objects.java index 47cd81207..eb475ac69 100644 --- a/src/main/java/io/jsonwebtoken/lang/Objects.java +++ b/src/main/java/io/jsonwebtoken/lang/Objects.java @@ -19,7 +19,6 @@ import java.io.IOException; import java.lang.reflect.Array; import java.util.Arrays; -import java.util.Collection; public final class Objects { @@ -96,16 +95,6 @@ public static boolean isEmpty(Object[] array) { return (array == null || array.length == 0); } - /** - * Determine whether the given collection is empty: - * i.e. null or of zero length. - * - * @param array the Collection to check - */ - public static boolean isEmpty(Collection collection) { - return (collection == null || collection.isEmpty()); - } - /** * Returns {@code true} if the specified byte array is null or of zero length, {@code false} otherwise. * From e8ba6192f3b28ac3297bb664a94bb067650ad57d Mon Sep 17 00:00:00 2001 From: Paul Cook Date: Wed, 26 Jul 2017 15:56:37 +0100 Subject: [PATCH 06/19] tests --- .../jsonwebtoken/impl/crypto/MacProvider.java | 37 +++++++++++++++++++ ...efaultSignatureValidatorFactoryTest.groovy | 2 +- 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/src/main/java/io/jsonwebtoken/impl/crypto/MacProvider.java b/src/main/java/io/jsonwebtoken/impl/crypto/MacProvider.java index 1ce280bb9..98bdd73f0 100644 --- a/src/main/java/io/jsonwebtoken/impl/crypto/MacProvider.java +++ b/src/main/java/io/jsonwebtoken/impl/crypto/MacProvider.java @@ -22,6 +22,8 @@ import javax.crypto.spec.SecretKeySpec; import java.security.Key; import java.security.SecureRandom; +import java.util.ArrayList; +import java.util.Collection; public abstract class MacProvider extends SignatureProvider { @@ -44,6 +46,20 @@ public static SecretKey generateKey() { return generateKey(SignatureAlgorithm.HS512); } + /** + * Generates a collection of new secure-random 512 bit secret key suitable for creating and verifying HMAC signatures. This is a + * convenience method that immediately delegates to {@link #generateKey(SignatureAlgorithm)} using {@link + * SignatureAlgorithm#HS512} as the method argument. + * + * @return a new secure-random 512 bit secret key suitable for creating and verifying HMAC signatures. + * @see #generateKey(SignatureAlgorithm) + * @see #generateKey(SignatureAlgorithm, SecureRandom) + * @since 0.5 + */ + public static Collection generateKeys(int howMany) { + return generateKeys(SignatureAlgorithm.HS512, howMany); + } + /** * Generates a new secure-random secret key of a length suitable for creating and verifying HMAC signatures * according to the specified {@code SignatureAlgorithm} using JJWT's default {@link @@ -62,6 +78,27 @@ public static SecretKey generateKey(SignatureAlgorithm alg) { return generateKey(alg, SignatureProvider.DEFAULT_SECURE_RANDOM); } + /** + * Generates a new secure-random secret key of a length suitable for creating and verifying HMAC signatures + * according to the specified {@code SignatureAlgorithm} using JJWT's default {@link + * SignatureProvider#DEFAULT_SECURE_RANDOM SecureRandom instance}. This is a convenience method that immediately + * delegates to {@link #generateKey(SignatureAlgorithm, SecureRandom)}. + * + * @param alg the desired signature algorithm + * @return a new secure-random secret key of a length suitable for creating and verifying HMAC signatures according + * to the specified {@code SignatureAlgorithm} using JJWT's default {@link SignatureProvider#DEFAULT_SECURE_RANDOM + * SecureRandom instance}. + * @see #generateKey() + * @see #generateKey(SignatureAlgorithm, SecureRandom) + * @since 0.5 + */ + public static Collection generateKeys(SignatureAlgorithm alg, int howMany) { + Collection keys = new ArrayList(); + for (int i=0;i Date: Wed, 26 Jul 2017 15:59:28 +0100 Subject: [PATCH 07/19] Revert "tests" This reverts commit e8ba6192f3b28ac3297bb664a94bb067650ad57d. --- .../jsonwebtoken/impl/crypto/MacProvider.java | 37 ------------------- ...efaultSignatureValidatorFactoryTest.groovy | 2 +- 2 files changed, 1 insertion(+), 38 deletions(-) diff --git a/src/main/java/io/jsonwebtoken/impl/crypto/MacProvider.java b/src/main/java/io/jsonwebtoken/impl/crypto/MacProvider.java index 98bdd73f0..1ce280bb9 100644 --- a/src/main/java/io/jsonwebtoken/impl/crypto/MacProvider.java +++ b/src/main/java/io/jsonwebtoken/impl/crypto/MacProvider.java @@ -22,8 +22,6 @@ import javax.crypto.spec.SecretKeySpec; import java.security.Key; import java.security.SecureRandom; -import java.util.ArrayList; -import java.util.Collection; public abstract class MacProvider extends SignatureProvider { @@ -46,20 +44,6 @@ public static SecretKey generateKey() { return generateKey(SignatureAlgorithm.HS512); } - /** - * Generates a collection of new secure-random 512 bit secret key suitable for creating and verifying HMAC signatures. This is a - * convenience method that immediately delegates to {@link #generateKey(SignatureAlgorithm)} using {@link - * SignatureAlgorithm#HS512} as the method argument. - * - * @return a new secure-random 512 bit secret key suitable for creating and verifying HMAC signatures. - * @see #generateKey(SignatureAlgorithm) - * @see #generateKey(SignatureAlgorithm, SecureRandom) - * @since 0.5 - */ - public static Collection generateKeys(int howMany) { - return generateKeys(SignatureAlgorithm.HS512, howMany); - } - /** * Generates a new secure-random secret key of a length suitable for creating and verifying HMAC signatures * according to the specified {@code SignatureAlgorithm} using JJWT's default {@link @@ -78,27 +62,6 @@ public static SecretKey generateKey(SignatureAlgorithm alg) { return generateKey(alg, SignatureProvider.DEFAULT_SECURE_RANDOM); } - /** - * Generates a new secure-random secret key of a length suitable for creating and verifying HMAC signatures - * according to the specified {@code SignatureAlgorithm} using JJWT's default {@link - * SignatureProvider#DEFAULT_SECURE_RANDOM SecureRandom instance}. This is a convenience method that immediately - * delegates to {@link #generateKey(SignatureAlgorithm, SecureRandom)}. - * - * @param alg the desired signature algorithm - * @return a new secure-random secret key of a length suitable for creating and verifying HMAC signatures according - * to the specified {@code SignatureAlgorithm} using JJWT's default {@link SignatureProvider#DEFAULT_SECURE_RANDOM - * SecureRandom instance}. - * @see #generateKey() - * @see #generateKey(SignatureAlgorithm, SecureRandom) - * @since 0.5 - */ - public static Collection generateKeys(SignatureAlgorithm alg, int howMany) { - Collection keys = new ArrayList(); - for (int i=0;i Date: Wed, 26 Jul 2017 16:17:07 +0100 Subject: [PATCH 08/19] support multiple private keys when validating signature of a token, each private key will be attempted in turn --- .../io/jsonwebtoken/SigningKeyResolver.java | 24 +++++ .../SigningKeyResolverAdapter.java | 71 +++++++++++++++ .../jsonwebtoken/impl/DefaultJwtParser.java | 88 ++++++++++++------- .../java/io/jsonwebtoken/lang/Objects.java | 11 +++ 4 files changed, 164 insertions(+), 30 deletions(-) diff --git a/src/main/java/io/jsonwebtoken/SigningKeyResolver.java b/src/main/java/io/jsonwebtoken/SigningKeyResolver.java index b068db9d2..789a9c2c8 100644 --- a/src/main/java/io/jsonwebtoken/SigningKeyResolver.java +++ b/src/main/java/io/jsonwebtoken/SigningKeyResolver.java @@ -16,6 +16,7 @@ package io.jsonwebtoken; import java.security.Key; +import java.util.Collection; /** * A {@code SigningKeyResolver} can be used by a {@link io.jsonwebtoken.JwtParser JwtParser} to find a signing key that @@ -60,6 +61,17 @@ public interface SigningKeyResolver { */ Key resolveSigningKey(JwsHeader header, Claims claims); + /** + * Returns a collection signing key that should be used to attempt to validate a digital signature for the Claims JWS with the specified + * header and claims. This allows for a key rotation scenario to support multiple keys during an overlap period. + * + * @param header the header of the JWS to validate + * @param claims the claims (body) of the JWS to validate + * @return the signing key that should be used to validate a digital signature for the Claims JWS with the specified + * header and claims. + */ + Collection resolveSigningKeys(JwsHeader header, Claims claims); + /** * Returns the signing key that should be used to validate a digital signature for the Plaintext JWS with the * specified header and plaintext payload. @@ -70,4 +82,16 @@ public interface SigningKeyResolver { * specified header and plaintext payload. */ Key resolveSigningKey(JwsHeader header, String plaintext); + + /** + * Returns the signing key that should be used to attempt to validate a digital signature for the Plaintext JWS with the + * specified header and plaintext payload. This allows for a key rotation scenario to support multiple keys during an overlap period. + * + * @param header the header of the JWS to validate + * @param plaintext the plaintext body of the JWS to validate + * @return the signing key that should be used to validate a digital signature for the Plaintext JWS with the + * specified header and plaintext payload. + */ + Collection resolveSigningKeys(JwsHeader header, String plaintext); + } diff --git a/src/main/java/io/jsonwebtoken/SigningKeyResolverAdapter.java b/src/main/java/io/jsonwebtoken/SigningKeyResolverAdapter.java index 1be7ec556..de9631f64 100644 --- a/src/main/java/io/jsonwebtoken/SigningKeyResolverAdapter.java +++ b/src/main/java/io/jsonwebtoken/SigningKeyResolverAdapter.java @@ -19,6 +19,8 @@ import javax.crypto.spec.SecretKeySpec; import java.security.Key; +import java.util.ArrayList; +import java.util.Collection; /** * An Adapter implementation of the @@ -51,6 +53,23 @@ public Key resolveSigningKey(JwsHeader header, Claims claims) { return new SecretKeySpec(keyBytes, alg.getJcaName()); } + @Override + public Collection resolveSigningKeys(JwsHeader header, Claims claims) { + SignatureAlgorithm alg = SignatureAlgorithm.forName(header.getAlgorithm()); + Assert.isTrue(alg.isHmac(), "The default resolveSigningKey(JwsHeader, Claims) implementation cannot be " + + "used for asymmetric key algorithms (RSA, Elliptic Curve). " + + "Override the resolveSigningKey(JwsHeader, Claims) method instead and return a " + + "Key instance appropriate for the " + alg.name() + " algorithm."); + Collection keysBytes = resolveSigningKeysBytes(header, claims); + if (keysBytes == null) + return null; + Collection keys = new ArrayList(); + for (byte[] keyBytes: keysBytes) + keys.add(new SecretKeySpec(keyBytes, alg.getJcaName())); + + return keys; + } + @Override public Key resolveSigningKey(JwsHeader header, String plaintext) { SignatureAlgorithm alg = SignatureAlgorithm.forName(header.getAlgorithm()); @@ -62,6 +81,23 @@ public Key resolveSigningKey(JwsHeader header, String plaintext) { return new SecretKeySpec(keyBytes, alg.getJcaName()); } + @Override + public Collection resolveSigningKeys(JwsHeader header, String plaintext) { + SignatureAlgorithm alg = SignatureAlgorithm.forName(header.getAlgorithm()); + Assert.isTrue(alg.isHmac(), "The default resolveSigningKey(JwsHeader, String) implementation cannot be " + + "used for asymmetric key algorithms (RSA, Elliptic Curve). " + + "Override the resolveSigningKey(JwsHeader, String) method instead and return a " + + "Key instance appropriate for the " + alg.name() + " algorithm."); + Collection keysBytes = resolveSigningKeysBytes(header, plaintext); + if (keysBytes == null) + return null; + Collection keys = new ArrayList(); + for (byte[] keyBytes: keysBytes) + keys.add(new SecretKeySpec(keyBytes, alg.getJcaName())); + + return keys; + } + /** * Convenience method invoked by {@link #resolveSigningKey(JwsHeader, Claims)} that obtains the necessary signing * key bytes. This implementation simply throws an exception: if the JWS parsed is a Claims JWS, you must @@ -81,6 +117,25 @@ public byte[] resolveSigningKeyBytes(JwsHeader header, Claims claims) { "resolveSigningKeyBytes(JwsHeader, Claims) method."); } + /** + * Convenience method invoked by {@link #resolveSigningKey(JwsHeader, Claims)} that obtains the necessary signing + * key bytes. This implementation simply throws an exception: if the JWS parsed is a Claims JWS, you must + * override this method or the {@link #resolveSigningKey(JwsHeader, Claims)} method instead. + * + *

NOTE: You cannot override this method when validating RSA signatures. If you expect RSA signatures, + * you must override the {@link #resolveSigningKey(JwsHeader, Claims)} method instead.

+ * + * @param header the parsed {@link JwsHeader} + * @param claims the parsed {@link Claims} + * @return the signing key bytes to use to verify the JWS signature. + */ + public Collection resolveSigningKeysBytes(JwsHeader header, Claims claims) { + throw new UnsupportedJwtException("The specified SigningKeyResolver implementation does not support " + + "Claims JWS signing key resolution. Consider overriding either the " + + "resolveSigningKey(JwsHeader, Claims) method or, for HMAC algorithms, the " + + "resolveSigningKeyBytes(JwsHeader, Claims) method."); + } + /** * Convenience method invoked by {@link #resolveSigningKey(JwsHeader, String)} that obtains the necessary signing * key bytes. This implementation simply throws an exception: if the JWS parsed is a plaintext JWS, you must @@ -96,4 +151,20 @@ public byte[] resolveSigningKeyBytes(JwsHeader header, String payload) { "resolveSigningKey(JwsHeader, String) method or, for HMAC algorithms, the " + "resolveSigningKeyBytes(JwsHeader, String) method."); } + + /** + * Convenience method invoked by {@link #resolveSigningKey(JwsHeader, String)} that obtains the necessary signing + * key bytes. This implementation simply throws an exception: if the JWS parsed is a plaintext JWS, you must + * override this method or the {@link #resolveSigningKey(JwsHeader, String)} method instead. + * + * @param header the parsed {@link JwsHeader} + * @param payload the parsed String plaintext payload + * @return the signing key bytes to use to verify the JWS signature. + */ + public Collection resolveSigningKeysBytes(JwsHeader header, String payload) { + throw new UnsupportedJwtException("The specified SigningKeyResolver implementation does not support " + + "plaintext JWS signing key resolution. Consider overriding either the " + + "resolveSigningKey(JwsHeader, String) method or, for HMAC algorithms, the " + + "resolveSigningKeyBytes(JwsHeader, String) method."); + } } diff --git a/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java b/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java index 4e4b9c79c..d3eb256bb 100644 --- a/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java +++ b/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java @@ -49,6 +49,8 @@ import java.io.IOException; import java.security.Key; import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Collection; import java.util.Date; import java.util.Map; @@ -61,9 +63,9 @@ public class DefaultJwtParser implements JwtParser { private ObjectMapper objectMapper = new ObjectMapper(); - private byte[] keyBytes; + private Collection keyBytes; - private Key key; + private Collection keys; private SigningKeyResolver signingKeyResolver; @@ -141,21 +143,27 @@ public JwtParser setAllowedClockSkewSeconds(long seconds) { @Override public JwtParser setSigningKey(byte[] key) { Assert.notEmpty(key, "signing key cannot be null or empty."); - this.keyBytes = key; + if (this.keyBytes == null) + this.keyBytes = new ArrayList(); + this.keyBytes.add(key); return this; } @Override public JwtParser setSigningKey(String base64EncodedKeyBytes) { Assert.hasText(base64EncodedKeyBytes, "signing key cannot be null or empty."); - this.keyBytes = TextCodec.BASE64.decode(base64EncodedKeyBytes); + if (this.keyBytes == null) + this.keyBytes = new ArrayList(); + this.keyBytes.add(TextCodec.BASE64.decode(base64EncodedKeyBytes)); return this; } @Override public JwtParser setSigningKey(Key key) { Assert.notNull(key, "signing key cannot be null."); - this.key = key; + if (this.keys == null) + this.keys = new ArrayList(); + this.keys.add(key); return this; } @@ -297,25 +305,36 @@ public Jwt parse(String jwt) throws ExpiredJwtException, MalformedJwtException, throw new MalformedJwtException(msg); } - if (key != null && keyBytes != null) { + if (keys != null && keyBytes != null) { throw new IllegalStateException("A key object and key bytes cannot both be specified. Choose either."); - } else if ((key != null || keyBytes != null) && signingKeyResolver != null) { - String object = key != null ? "a key object" : "key bytes"; + } else if ((keys != null || keyBytes != null) && signingKeyResolver != null) { + String object = keys != null ? "a key object" : "key bytes"; throw new IllegalStateException("A signing key resolver and " + object + " cannot both be specified. Choose either."); } //digitally signed, let's assert the signature: - Key key = this.key; + Collection keys = this.keys; - if (key == null) { //fall back to keyBytes + if (keys == null) { //fall back to keyBytes - byte[] keyBytes = this.keyBytes; + //byte[] keyBytes = this.keyBytes; if (Objects.isEmpty(keyBytes) && signingKeyResolver != null) { //use the signingKeyResolver + keys = new ArrayList(); if (claims != null) { - key = signingKeyResolver.resolveSigningKey(jwsHeader, claims); + Key key = this.signingKeyResolver.resolveSigningKey(jwsHeader, claims); + if (key != null) + keys.add(key); + Collection keyList = this.signingKeyResolver.resolveSigningKeys(jwsHeader, claims); + if (!Objects.isEmpty(keyList)) + keys.addAll(keyList); } else { - key = signingKeyResolver.resolveSigningKey(jwsHeader, payload); + Key key = this.signingKeyResolver.resolveSigningKey(jwsHeader, payload); + if (key != null) + keys.add(key); + Collection keyList = this.signingKeyResolver.resolveSigningKeys(jwsHeader, payload); + if (!Objects.isEmpty(keyList)) + keys.addAll(keyList); } } @@ -324,31 +343,40 @@ public Jwt parse(String jwt) throws ExpiredJwtException, MalformedJwtException, Assert.isTrue(algorithm.isHmac(), "Key bytes can only be specified for HMAC signatures. Please specify a PublicKey or PrivateKey instance."); - key = new SecretKeySpec(keyBytes, algorithm.getJcaName()); + keys = new ArrayList(); + for (byte[] bytes: this.keyBytes) + keys.add(new SecretKeySpec(bytes, algorithm.getJcaName())); } } - Assert.notNull(key, "A signing key must be specified if the specified JWT is digitally signed."); + Assert.notNull(keys, "A signing key must be specified if the specified JWT is digitally signed."); //re-create the jwt part without the signature. This is what needs to be signed for verification: String jwtWithoutSignature = base64UrlEncodedHeader + SEPARATOR_CHAR + base64UrlEncodedPayload; - JwtSignatureValidator validator; - try { - validator = createSignatureValidator(algorithm, key); - } catch (IllegalArgumentException e) { - String algName = algorithm.getValue(); - String msg = "The parsed JWT indicates it was signed with the " + algName + " signature " + - "algorithm, but the specified signing key of type " + key.getClass().getName() + - " may not be used to validate " + algName + " signatures. Because the specified " + - "signing key reflects a specific and expected algorithm, and the JWT does not reflect " + - "this algorithm, it is likely that the JWT was not expected and therefore should not be " + - "trusted. Another possibility is that the parser was configured with the incorrect " + - "signing key, but this cannot be assumed for security reasons."; - throw new UnsupportedJwtException(msg, e); + boolean signatureOk = false; + for (Key key: keys) { + JwtSignatureValidator validator; + try { + validator = createSignatureValidator(algorithm, key); + } catch (IllegalArgumentException e) { + String algName = algorithm.getValue(); + String msg = "The parsed JWT indicates it was signed with the " + algName + " signature " + + "algorithm, but the specified signing key of type " + key.getClass().getName() + + " may not be used to validate " + algName + " signatures. Because the specified " + + "signing key reflects a specific and expected algorithm, and the JWT does not reflect " + + "this algorithm, it is likely that the JWT was not expected and therefore should not be " + + "trusted. Another possibility is that the parser was configured with the incorrect " + + "signing key, but this cannot be assumed for security reasons."; + throw new UnsupportedJwtException(msg, e); + } + + if (validator.isValid(jwtWithoutSignature, base64UrlEncodedDigest)) { + signatureOk = true; + break; + } } - - if (!validator.isValid(jwtWithoutSignature, base64UrlEncodedDigest)) { + if (!signatureOk) { String msg = "JWT signature does not match locally computed signature. JWT validity cannot be " + "asserted and should not be trusted."; throw new SignatureException(msg); diff --git a/src/main/java/io/jsonwebtoken/lang/Objects.java b/src/main/java/io/jsonwebtoken/lang/Objects.java index eb475ac69..3038b5bb9 100644 --- a/src/main/java/io/jsonwebtoken/lang/Objects.java +++ b/src/main/java/io/jsonwebtoken/lang/Objects.java @@ -19,6 +19,7 @@ import java.io.IOException; import java.lang.reflect.Array; import java.util.Arrays; +import java.util.Collection; public final class Objects { @@ -95,6 +96,16 @@ public static boolean isEmpty(Object[] array) { return (array == null || array.length == 0); } + /** + * Determine whether the given collection is empty: + * i.e. null or of zero length. + * + * @param array the Collection to check + */ + public static boolean isEmpty(Collection collection) { + return (collection == null || collection.isEmpty()); + } + /** * Returns {@code true} if the specified byte array is null or of zero length, {@code false} otherwise. * From c1e9e9e60e7dc8dd17c995e895c7f1e651a9b310 Mon Sep 17 00:00:00 2001 From: Paul Cook Date: Wed, 26 Jul 2017 16:34:09 +0100 Subject: [PATCH 09/19] only retrieve either a single key or a collection of keys from the signing key resolver this allows existing subclasses of the SigningKeyResolverAdaptor to work un-modified with these changes the new methods are only necessary to override if you wish to take advantage of the ability to supply a collection of keys --- .../io/jsonwebtoken/impl/DefaultJwtParser.java | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java b/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java index d3eb256bb..4af6fcf2c 100644 --- a/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java +++ b/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java @@ -325,16 +325,20 @@ public Jwt parse(String jwt) throws ExpiredJwtException, MalformedJwtException, Key key = this.signingKeyResolver.resolveSigningKey(jwsHeader, claims); if (key != null) keys.add(key); - Collection keyList = this.signingKeyResolver.resolveSigningKeys(jwsHeader, claims); - if (!Objects.isEmpty(keyList)) - keys.addAll(keyList); + else { + Collection keyList = this.signingKeyResolver.resolveSigningKeys(jwsHeader, claims); + if (!Objects.isEmpty(keyList)) + keys.addAll(keyList); + } } else { Key key = this.signingKeyResolver.resolveSigningKey(jwsHeader, payload); if (key != null) keys.add(key); - Collection keyList = this.signingKeyResolver.resolveSigningKeys(jwsHeader, payload); - if (!Objects.isEmpty(keyList)) - keys.addAll(keyList); + else { + Collection keyList = this.signingKeyResolver.resolveSigningKeys(jwsHeader, payload); + if (!Objects.isEmpty(keyList)) + keys.addAll(keyList); + } } } From ef42b0d616af4bc59b5c81e5830413113cb0d0b3 Mon Sep 17 00:00:00 2001 From: Paul Cook Date: Wed, 26 Jul 2017 17:23:30 +0100 Subject: [PATCH 10/19] test coverage --- .../SigningKeyResolverAdapter.java | 4 + .../io/jsonwebtoken/JwtParserTest.groovy | 182 ++++++++++++++++++ 2 files changed, 186 insertions(+) diff --git a/src/main/java/io/jsonwebtoken/SigningKeyResolverAdapter.java b/src/main/java/io/jsonwebtoken/SigningKeyResolverAdapter.java index de9631f64..53da6db44 100644 --- a/src/main/java/io/jsonwebtoken/SigningKeyResolverAdapter.java +++ b/src/main/java/io/jsonwebtoken/SigningKeyResolverAdapter.java @@ -50,6 +50,8 @@ public Key resolveSigningKey(JwsHeader header, Claims claims) { "Override the resolveSigningKey(JwsHeader, Claims) method instead and return a " + "Key instance appropriate for the " + alg.name() + " algorithm."); byte[] keyBytes = resolveSigningKeyBytes(header, claims); + if (keyBytes == null) + return null; return new SecretKeySpec(keyBytes, alg.getJcaName()); } @@ -78,6 +80,8 @@ public Key resolveSigningKey(JwsHeader header, String plaintext) { "Override the resolveSigningKey(JwsHeader, String) method instead and return a " + "Key instance appropriate for the " + alg.name() + " algorithm."); byte[] keyBytes = resolveSigningKeyBytes(header, plaintext); + if (keyBytes == null) + return null; return new SecretKeySpec(keyBytes, alg.getJcaName()); } diff --git a/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy b/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy index 187711fec..0e50ed540 100644 --- a/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy +++ b/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy @@ -606,6 +606,35 @@ class JwtParserTest { assertEquals jws.getBody().getSubject(), subject } + @Test + void testParseClaimsWithSigningKeysResolver() { + + String subject = 'Joe' + + byte[] key = randomKey() + byte[] key2 = randomKey() + + String compact = Jwts.builder().setSubject(subject).signWith(SignatureAlgorithm.HS256, key).compact() + + def signingKeyResolver = new SigningKeyResolverAdapter() { + @Override + byte[] resolveSigningKeyBytes(JwsHeader header, Claims claims) { + return null + } + @Override + Collection resolveSigningKeysBytes(JwsHeader header, Claims claims) { + ArrayList list = new ArrayList() + list.add(key2) + list.add(key) + return list + } + } + + Jws jws = Jwts.parser().setSigningKeyResolver(signingKeyResolver).parseClaimsJws(compact) + + assertEquals jws.getBody().getSubject(), subject + } + @Test void testParseClaimsWithSigningKeyResolverInvalidKey() { @@ -630,6 +659,37 @@ class JwtParserTest { } } + @Test + void testParseClaimsWithSigningKeyResolverInvalidKeys() { + + String subject = 'Joe' + + byte[] key = randomKey() + + String compact = Jwts.builder().setSubject(subject).signWith(SignatureAlgorithm.HS256, key).compact() + + def signingKeyResolver = new SigningKeyResolverAdapter() { + @Override + byte[] resolveSigningKeyBytes(JwsHeader header, Claims claims) { + return null + } + @Override + Collection resolveSigningKeysBytes(JwsHeader header, Claims claims) { + ArrayList list = new ArrayList() + list.add(randomKey()) + list.add(randomKey()) + return list + } + } + + try { + Jwts.parser().setSigningKeyResolver(signingKeyResolver).parseClaimsJws(compact) + fail() + } catch (SignatureException se) { + assertEquals se.getMessage(), 'JWT signature does not match locally computed signature. JWT validity cannot be asserted and should not be trusted.' + } + } + @Test void testParseClaimsWithSigningKeyResolverAndKey() { @@ -654,6 +714,37 @@ class JwtParserTest { } } + @Test + void testParseClaimsWithSigningKeyResolverAndKeys() { + + String subject = 'Joe' + + SecretKeySpec key = new SecretKeySpec(randomKey(), "HmacSHA256") + + String compact = Jwts.builder().setSubject(subject).signWith(SignatureAlgorithm.HS256, key).compact() + + def signingKeyResolver = new SigningKeyResolverAdapter() { + @Override + byte[] resolveSigningKeyBytes(JwsHeader header, Claims claims) { + return null + } + @Override + Collection resolveSigningKeysBytes(JwsHeader header, Claims claims) { + ArrayList list = new ArrayList() + list.add(randomKey()) + list.add(randomKey()) + return list + } + } + + try { + Jwts.parser().setSigningKey(key).setSigningKeyResolver(signingKeyResolver).parseClaimsJws(compact) + fail() + } catch (IllegalStateException ise) { + assertEquals ise.getMessage(), 'A signing key resolver and a key object cannot both be specified. Choose either.' + } + } + @Test void testParseClaimsWithSigningKeyResolverAndKeyBytes() { @@ -678,6 +769,37 @@ class JwtParserTest { } } + @Test + void testParseClaimsWithSigningKeyResolverAndKeysBytes() { + + String subject = 'Joe' + + byte[] key = randomKey() + + String compact = Jwts.builder().setSubject(subject).signWith(SignatureAlgorithm.HS256, key).compact() + + def signingKeyResolver = new SigningKeyResolverAdapter() { + @Override + byte[] resolveSigningKeyBytes(JwsHeader header, Claims claims) { + return null + } + @Override + Collection resolveSigningKeysBytes(JwsHeader header, Claims claims) { + ArrayList list = new ArrayList() + list.add(randomKey()) + list.add(randomKey()) + return list + } + } + + try { + Jwts.parser().setSigningKey(key).setSigningKeyResolver(signingKeyResolver).parseClaimsJws(compact) + fail() + } catch (IllegalStateException ise) { + assertEquals ise.getMessage(), 'A signing key resolver and key bytes cannot both be specified. Choose either.' + } + } + @Test void testParseClaimsWithNullSigningKeyResolver() { @@ -772,6 +894,35 @@ class JwtParserTest { assertEquals jws.getBody(), inputPayload } + @Test + void testParsePlaintextJwsWithSigningKeyResolverAdapterMultipleKeys() { + + String inputPayload = 'Hello world!' + + byte[] key = randomKey() + byte[] key2 = randomKey() + + String compact = Jwts.builder().setPayload(inputPayload).signWith(SignatureAlgorithm.HS256, key).compact() + + def signingKeyResolver = new SigningKeyResolverAdapter() { + @Override + byte[] resolveSigningKeyBytes(JwsHeader header, String payload) { + return null + } + @Override + Collection resolveSigningKeysBytes(JwsHeader header, String payload) { + ArrayList list = new ArrayList() + list.add(key2) + list.add(key) + return list + } + } + + Jws jws = Jwts.parser().setSigningKeyResolver(signingKeyResolver).parsePlaintextJws(compact) + + assertEquals jws.getBody(), inputPayload + } + @Test void testParsePlaintextJwsWithSigningKeyResolverInvalidKey() { @@ -796,6 +947,37 @@ class JwtParserTest { } } + @Test + void testParsePlaintextJwsWithSigningKeyResolverInvalidMultipleKey() { + + String inputPayload = 'Hello world!' + + byte[] key = randomKey() + + String compact = Jwts.builder().setPayload(inputPayload).signWith(SignatureAlgorithm.HS256, key).compact() + + def signingKeyResolver = new SigningKeyResolverAdapter() { + @Override + byte[] resolveSigningKeyBytes(JwsHeader header, String payload) { + return null + } + @Override + Collection resolveSigningKeysBytes(JwsHeader header, String payload) { + ArrayList list = new ArrayList() + list.add(randomKey()) + list.add(randomKey()) + return list + } + } + + try { + Jwts.parser().setSigningKeyResolver(signingKeyResolver).parsePlaintextJws(compact) + fail() + } catch (SignatureException se) { + assertEquals se.getMessage(), 'JWT signature does not match locally computed signature. JWT validity cannot be asserted and should not be trusted.' + } + } + @Test void testParsePlaintextJwsWithInvalidSigningKeyResolverAdapter() { From 35c674e3b4f021731b2c9c6363cee6d66e37f52d Mon Sep 17 00:00:00 2001 From: Paul Cook Date: Thu, 27 Jul 2017 15:21:47 +0100 Subject: [PATCH 11/19] test coverage --- .../io/jsonwebtoken/JwtParserTest.groovy | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy b/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy index 0e50ed540..550c55620 100644 --- a/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy +++ b/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy @@ -999,6 +999,58 @@ class JwtParserTest { } } + @Test + void testParsePlaintextJwsWithSigningKeyResolverAdapterWithNoKey() { + + String inputPayload = 'Hello world!' + + byte[] key = randomKey() + + String compact = Jwts.builder().setPayload(inputPayload).signWith(SignatureAlgorithm.HS256, key).compact() + + def signingKeyResolver = new SigningKeyResolverAdapter() { + @Override + byte[] resolveSigningKeyBytes(JwsHeader header, String payload) { + return null + } + } + + try { + Jwts.parser().setSigningKeyResolver(signingKeyResolver).parsePlaintextJws(compact) + fail() + } catch (UnsupportedJwtException ex) { + assertEquals ex.getMessage(), 'The specified SigningKeyResolver implementation does not support plaintext ' + + 'JWS signing key resolution. Consider overriding either the resolveSigningKey(JwsHeader, String) ' + + 'method or, for HMAC algorithms, the resolveSigningKeyBytes(JwsHeader, String) method.' + } + } + + @Test + void testParsePlaintextJwsWithSigningKeyResolverAdapterWithNoKeys() { + + String inputPayload = 'Hello world!' + + byte[] key = randomKey() + + String compact = Jwts.builder().setPayload(inputPayload).signWith(SignatureAlgorithm.HS256, key).compact() + + def signingKeyResolver = new SigningKeyResolverAdapter() { + @Override + Collection resolveSigningKeysBytes(JwsHeader header, String payload) { + return null + } + } + + try { + Jwts.parser().setSigningKeyResolver(signingKeyResolver).parsePlaintextJws(compact) + fail() + } catch (UnsupportedJwtException ex) { + assertEquals ex.getMessage(), 'The specified SigningKeyResolver implementation does not support plaintext ' + + 'JWS signing key resolution. Consider overriding either the resolveSigningKey(JwsHeader, String) ' + + 'method or, for HMAC algorithms, the resolveSigningKeyBytes(JwsHeader, String) method.' + } + } + @Test void testParseRequireDontAllowNullClaimName() { def expectedClaimValue = 'A Most Awesome Claim Value' From 2191bf8a5a64fa43932da220bc8582dcea4c9eb9 Mon Sep 17 00:00:00 2001 From: Paul Cook Date: Thu, 27 Jul 2017 15:31:19 +0100 Subject: [PATCH 12/19] re run the tests --- src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy b/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy index 550c55620..122684b06 100644 --- a/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy +++ b/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy @@ -1024,7 +1024,7 @@ class JwtParserTest { 'method or, for HMAC algorithms, the resolveSigningKeyBytes(JwsHeader, String) method.' } } - + @Test void testParsePlaintextJwsWithSigningKeyResolverAdapterWithNoKeys() { From 25860fb74897022d1061b4efcd2d72c2086f83eb Mon Sep 17 00:00:00 2001 From: Paul Cook Date: Thu, 27 Jul 2017 15:55:42 +0100 Subject: [PATCH 13/19] try both resolveKey and resolveKeys before throwing any exception generated by the SigningKeyResolverAdaptor --- .../jsonwebtoken/impl/DefaultJwtParser.java | 19 ++++++++++++++----- .../io/jsonwebtoken/JwtParserTest.groovy | 2 +- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java b/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java index 4af6fcf2c..a355eeb56 100644 --- a/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java +++ b/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java @@ -314,6 +314,7 @@ public Jwt parse(String jwt) throws ExpiredJwtException, MalformedJwtException, //digitally signed, let's assert the signature: Collection keys = this.keys; + UnsupportedJwtException signingKeyResolverException = null; if (keys == null) { //fall back to keyBytes @@ -322,13 +323,19 @@ public Jwt parse(String jwt) throws ExpiredJwtException, MalformedJwtException, if (Objects.isEmpty(keyBytes) && signingKeyResolver != null) { //use the signingKeyResolver keys = new ArrayList(); if (claims != null) { - Key key = this.signingKeyResolver.resolveSigningKey(jwsHeader, claims); - if (key != null) - keys.add(key); - else { + try { + Key key = this.signingKeyResolver.resolveSigningKey(jwsHeader, claims); + if (key != null) + keys.add(key); + } catch (UnsupportedJwtException e) { + signingKeyResolverException = e; + } + try { Collection keyList = this.signingKeyResolver.resolveSigningKeys(jwsHeader, claims); if (!Objects.isEmpty(keyList)) keys.addAll(keyList); + } catch (UnsupportedJwtException e) { + signingKeyResolverException = e; } } else { Key key = this.signingKeyResolver.resolveSigningKey(jwsHeader, payload); @@ -340,6 +347,8 @@ public Jwt parse(String jwt) throws ExpiredJwtException, MalformedJwtException, keys.addAll(keyList); } } + if (keys.size() == 0 && signingKeyResolverException != null) + throw signingKeyResolverException; } if (!Objects.isEmpty(keyBytes)) { @@ -352,7 +361,7 @@ public Jwt parse(String jwt) throws ExpiredJwtException, MalformedJwtException, keys.add(new SecretKeySpec(bytes, algorithm.getJcaName())); } } - + Assert.notNull(keys, "A signing key must be specified if the specified JWT is digitally signed."); //re-create the jwt part without the signature. This is what needs to be signed for verification: diff --git a/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy b/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy index 122684b06..550c55620 100644 --- a/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy +++ b/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy @@ -1024,7 +1024,7 @@ class JwtParserTest { 'method or, for HMAC algorithms, the resolveSigningKeyBytes(JwsHeader, String) method.' } } - + @Test void testParsePlaintextJwsWithSigningKeyResolverAdapterWithNoKeys() { From 3affd83313a2f5080ee1f03625c6401878fbc51e Mon Sep 17 00:00:00 2001 From: Paul Cook Date: Thu, 27 Jul 2017 16:05:37 +0100 Subject: [PATCH 14/19] allow both resolveKey and resolveKeys to be evanulated when parsing plaintext as well --- .../io/jsonwebtoken/impl/DefaultJwtParser.java | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java b/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java index a355eeb56..937c74029 100644 --- a/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java +++ b/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java @@ -338,13 +338,19 @@ public Jwt parse(String jwt) throws ExpiredJwtException, MalformedJwtException, signingKeyResolverException = e; } } else { - Key key = this.signingKeyResolver.resolveSigningKey(jwsHeader, payload); - if (key != null) - keys.add(key); - else { + try { + Key key = this.signingKeyResolver.resolveSigningKey(jwsHeader, payload); + if (key != null) + keys.add(key); + } catch (UnsupportedJwtException e) { + signingKeyResolverException = e; + } + try { Collection keyList = this.signingKeyResolver.resolveSigningKeys(jwsHeader, payload); if (!Objects.isEmpty(keyList)) keys.addAll(keyList); + } catch (UnsupportedJwtException e) { + signingKeyResolverException = e; } } if (keys.size() == 0 && signingKeyResolverException != null) From c1c25bab204c273b259402e505939853a677e4a3 Mon Sep 17 00:00:00 2001 From: Paul Cook Date: Thu, 27 Jul 2017 16:14:08 +0100 Subject: [PATCH 15/19] additional tests to increase coverage --- .../io/jsonwebtoken/JwtParserTest.groovy | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy b/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy index 550c55620..e5e089462 100644 --- a/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy +++ b/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy @@ -838,6 +838,59 @@ class JwtParserTest { } } + @Test + void testParseClaimsWithSigningKeyResolverAdapterWithNoKey() { + + + String subject = 'Joe' + + byte[] key = randomKey() + + String compact = Jwts.builder().setSubject(subject).signWith(SignatureAlgorithm.HS256, key).compact() + + def signingKeyResolver = new SigningKeyResolverAdapter() { + @Override + byte[] resolveSigningKeyBytes(JwsHeader header, Claims claims) { + return null + } + } + + try { + Jwts.parser().setSigningKeyResolver(signingKeyResolver).parseClaimsJws(compact) + fail() + } catch (UnsupportedJwtException ex) { + assertEquals ex.getMessage(), 'The specified SigningKeyResolver implementation does not support ' + + 'Claims JWS signing key resolution. Consider overriding either the resolveSigningKey(JwsHeader, Claims) method ' + + 'or, for HMAC algorithms, the resolveSigningKeyBytes(JwsHeader, Claims) method.' + } + } + + @Test + void testParseClaimsWithSigningKeyResolverAdapterWithNoKeys() { + + String subject = 'Joe' + + byte[] key = randomKey() + + String compact = Jwts.builder().setSubject(subject).signWith(SignatureAlgorithm.HS256, key).compact() + + def signingKeyResolver = new SigningKeyResolverAdapter() { + @Override + Collection resolveSigningKeysBytes(JwsHeader header, Claims claims) { + return null + } + } + + try { + Jwts.parser().setSigningKeyResolver(signingKeyResolver).parseClaimsJws(compact) + fail() + } catch (UnsupportedJwtException ex) { + assertEquals ex.getMessage(), 'The specified SigningKeyResolver implementation does not support ' + + 'Claims JWS signing key resolution. Consider overriding either the resolveSigningKey(JwsHeader, Claims) method ' + + 'or, for HMAC algorithms, the resolveSigningKeyBytes(JwsHeader, Claims) method.' + } + } + @Test void testParseClaimsJwsWithNumericTypes() { byte[] key = randomKey() From 1e22d8fa619adcf740bd73d573373cc6f7fc26ac Mon Sep 17 00:00:00 2001 From: Paul Cook Date: Thu, 27 Jul 2017 16:20:24 +0100 Subject: [PATCH 16/19] re trigger tests --- src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy b/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy index e5e089462..4ab685f4a 100644 --- a/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy +++ b/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy @@ -880,7 +880,7 @@ class JwtParserTest { return null } } - + try { Jwts.parser().setSigningKeyResolver(signingKeyResolver).parseClaimsJws(compact) fail() From abde7e0ce0625f09dfaedd919ebe0acaaa5b96dc Mon Sep 17 00:00:00 2001 From: Paul Cook Date: Thu, 27 Jul 2017 16:33:51 +0100 Subject: [PATCH 17/19] explicitely use a Date for 'expiration' in the testParseRequireExpiration_Success test to address intermittent test failure --- src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy b/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy index 4ab685f4a..17071a7ae 100644 --- a/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy +++ b/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy @@ -1573,7 +1573,7 @@ class JwtParserTest { @Test void testParseRequireExpiration_Success() { // expire in the future - def expiration = new Date(System.currentTimeMillis() + 10000) + Date expiration = new Date(System.currentTimeMillis() + 10000) byte[] key = randomKey() From 9f0c6ee310d276a9e17f545841fcdbf9a85bb0df Mon Sep 17 00:00:00 2001 From: Paul Cook Date: Thu, 27 Jul 2017 18:34:38 +0100 Subject: [PATCH 18/19] revert everything --- .../io/jsonwebtoken/SigningKeyResolver.java | 24 ------- .../SigningKeyResolverAdapter.java | 71 ------------------- .../jsonwebtoken/impl/DefaultJwtParser.java | 62 ++++++---------- .../crypto/DefaultJwtSignatureValidator.java | 9 ++- .../DefaultSignatureValidatorFactory.java | 11 ++- .../EllipticCurveSignatureValidator.java | 45 +++++------- .../jsonwebtoken/impl/crypto/MacProvider.java | 37 ---------- .../impl/crypto/MacValidator.java | 20 ++---- .../impl/crypto/RsaSignatureValidator.java | 59 +++++---------- .../crypto/SignatureValidatorFactory.java | 3 +- .../java/io/jsonwebtoken/lang/Objects.java | 11 --- ...efaultSignatureValidatorFactoryTest.groovy | 2 +- 12 files changed, 74 insertions(+), 280 deletions(-) diff --git a/src/main/java/io/jsonwebtoken/SigningKeyResolver.java b/src/main/java/io/jsonwebtoken/SigningKeyResolver.java index 789a9c2c8..b068db9d2 100644 --- a/src/main/java/io/jsonwebtoken/SigningKeyResolver.java +++ b/src/main/java/io/jsonwebtoken/SigningKeyResolver.java @@ -16,7 +16,6 @@ package io.jsonwebtoken; import java.security.Key; -import java.util.Collection; /** * A {@code SigningKeyResolver} can be used by a {@link io.jsonwebtoken.JwtParser JwtParser} to find a signing key that @@ -61,17 +60,6 @@ public interface SigningKeyResolver { */ Key resolveSigningKey(JwsHeader header, Claims claims); - /** - * Returns a collection signing key that should be used to attempt to validate a digital signature for the Claims JWS with the specified - * header and claims. This allows for a key rotation scenario to support multiple keys during an overlap period. - * - * @param header the header of the JWS to validate - * @param claims the claims (body) of the JWS to validate - * @return the signing key that should be used to validate a digital signature for the Claims JWS with the specified - * header and claims. - */ - Collection resolveSigningKeys(JwsHeader header, Claims claims); - /** * Returns the signing key that should be used to validate a digital signature for the Plaintext JWS with the * specified header and plaintext payload. @@ -82,16 +70,4 @@ public interface SigningKeyResolver { * specified header and plaintext payload. */ Key resolveSigningKey(JwsHeader header, String plaintext); - - /** - * Returns the signing key that should be used to attempt to validate a digital signature for the Plaintext JWS with the - * specified header and plaintext payload. This allows for a key rotation scenario to support multiple keys during an overlap period. - * - * @param header the header of the JWS to validate - * @param plaintext the plaintext body of the JWS to validate - * @return the signing key that should be used to validate a digital signature for the Plaintext JWS with the - * specified header and plaintext payload. - */ - Collection resolveSigningKeys(JwsHeader header, String plaintext); - } diff --git a/src/main/java/io/jsonwebtoken/SigningKeyResolverAdapter.java b/src/main/java/io/jsonwebtoken/SigningKeyResolverAdapter.java index de9631f64..1be7ec556 100644 --- a/src/main/java/io/jsonwebtoken/SigningKeyResolverAdapter.java +++ b/src/main/java/io/jsonwebtoken/SigningKeyResolverAdapter.java @@ -19,8 +19,6 @@ import javax.crypto.spec.SecretKeySpec; import java.security.Key; -import java.util.ArrayList; -import java.util.Collection; /** * An Adapter implementation of the @@ -53,23 +51,6 @@ public Key resolveSigningKey(JwsHeader header, Claims claims) { return new SecretKeySpec(keyBytes, alg.getJcaName()); } - @Override - public Collection resolveSigningKeys(JwsHeader header, Claims claims) { - SignatureAlgorithm alg = SignatureAlgorithm.forName(header.getAlgorithm()); - Assert.isTrue(alg.isHmac(), "The default resolveSigningKey(JwsHeader, Claims) implementation cannot be " + - "used for asymmetric key algorithms (RSA, Elliptic Curve). " + - "Override the resolveSigningKey(JwsHeader, Claims) method instead and return a " + - "Key instance appropriate for the " + alg.name() + " algorithm."); - Collection keysBytes = resolveSigningKeysBytes(header, claims); - if (keysBytes == null) - return null; - Collection keys = new ArrayList(); - for (byte[] keyBytes: keysBytes) - keys.add(new SecretKeySpec(keyBytes, alg.getJcaName())); - - return keys; - } - @Override public Key resolveSigningKey(JwsHeader header, String plaintext) { SignatureAlgorithm alg = SignatureAlgorithm.forName(header.getAlgorithm()); @@ -81,23 +62,6 @@ public Key resolveSigningKey(JwsHeader header, String plaintext) { return new SecretKeySpec(keyBytes, alg.getJcaName()); } - @Override - public Collection resolveSigningKeys(JwsHeader header, String plaintext) { - SignatureAlgorithm alg = SignatureAlgorithm.forName(header.getAlgorithm()); - Assert.isTrue(alg.isHmac(), "The default resolveSigningKey(JwsHeader, String) implementation cannot be " + - "used for asymmetric key algorithms (RSA, Elliptic Curve). " + - "Override the resolveSigningKey(JwsHeader, String) method instead and return a " + - "Key instance appropriate for the " + alg.name() + " algorithm."); - Collection keysBytes = resolveSigningKeysBytes(header, plaintext); - if (keysBytes == null) - return null; - Collection keys = new ArrayList(); - for (byte[] keyBytes: keysBytes) - keys.add(new SecretKeySpec(keyBytes, alg.getJcaName())); - - return keys; - } - /** * Convenience method invoked by {@link #resolveSigningKey(JwsHeader, Claims)} that obtains the necessary signing * key bytes. This implementation simply throws an exception: if the JWS parsed is a Claims JWS, you must @@ -117,25 +81,6 @@ public byte[] resolveSigningKeyBytes(JwsHeader header, Claims claims) { "resolveSigningKeyBytes(JwsHeader, Claims) method."); } - /** - * Convenience method invoked by {@link #resolveSigningKey(JwsHeader, Claims)} that obtains the necessary signing - * key bytes. This implementation simply throws an exception: if the JWS parsed is a Claims JWS, you must - * override this method or the {@link #resolveSigningKey(JwsHeader, Claims)} method instead. - * - *

NOTE: You cannot override this method when validating RSA signatures. If you expect RSA signatures, - * you must override the {@link #resolveSigningKey(JwsHeader, Claims)} method instead.

- * - * @param header the parsed {@link JwsHeader} - * @param claims the parsed {@link Claims} - * @return the signing key bytes to use to verify the JWS signature. - */ - public Collection resolveSigningKeysBytes(JwsHeader header, Claims claims) { - throw new UnsupportedJwtException("The specified SigningKeyResolver implementation does not support " + - "Claims JWS signing key resolution. Consider overriding either the " + - "resolveSigningKey(JwsHeader, Claims) method or, for HMAC algorithms, the " + - "resolveSigningKeyBytes(JwsHeader, Claims) method."); - } - /** * Convenience method invoked by {@link #resolveSigningKey(JwsHeader, String)} that obtains the necessary signing * key bytes. This implementation simply throws an exception: if the JWS parsed is a plaintext JWS, you must @@ -151,20 +96,4 @@ public byte[] resolveSigningKeyBytes(JwsHeader header, String payload) { "resolveSigningKey(JwsHeader, String) method or, for HMAC algorithms, the " + "resolveSigningKeyBytes(JwsHeader, String) method."); } - - /** - * Convenience method invoked by {@link #resolveSigningKey(JwsHeader, String)} that obtains the necessary signing - * key bytes. This implementation simply throws an exception: if the JWS parsed is a plaintext JWS, you must - * override this method or the {@link #resolveSigningKey(JwsHeader, String)} method instead. - * - * @param header the parsed {@link JwsHeader} - * @param payload the parsed String plaintext payload - * @return the signing key bytes to use to verify the JWS signature. - */ - public Collection resolveSigningKeysBytes(JwsHeader header, String payload) { - throw new UnsupportedJwtException("The specified SigningKeyResolver implementation does not support " + - "plaintext JWS signing key resolution. Consider overriding either the " + - "resolveSigningKey(JwsHeader, String) method or, for HMAC algorithms, the " + - "resolveSigningKeyBytes(JwsHeader, String) method."); - } } diff --git a/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java b/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java index 40b5aaa32..4e4b9c79c 100644 --- a/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java +++ b/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java @@ -49,8 +49,6 @@ import java.io.IOException; import java.security.Key; import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.Collection; import java.util.Date; import java.util.Map; @@ -63,9 +61,9 @@ public class DefaultJwtParser implements JwtParser { private ObjectMapper objectMapper = new ObjectMapper(); - private Collection keyBytes; + private byte[] keyBytes; - private Collection keys; + private Key key; private SigningKeyResolver signingKeyResolver; @@ -143,27 +141,21 @@ public JwtParser setAllowedClockSkewSeconds(long seconds) { @Override public JwtParser setSigningKey(byte[] key) { Assert.notEmpty(key, "signing key cannot be null or empty."); - if (this.keyBytes == null) - this.keyBytes = new ArrayList(); - this.keyBytes.add(key); + this.keyBytes = key; return this; } @Override public JwtParser setSigningKey(String base64EncodedKeyBytes) { Assert.hasText(base64EncodedKeyBytes, "signing key cannot be null or empty."); - if (this.keyBytes == null) - this.keyBytes = new ArrayList(); - this.keyBytes.add(TextCodec.BASE64.decode(base64EncodedKeyBytes)); + this.keyBytes = TextCodec.BASE64.decode(base64EncodedKeyBytes); return this; } @Override public JwtParser setSigningKey(Key key) { Assert.notNull(key, "signing key cannot be null."); - if (this.keys == null) - this.keys = new ArrayList(); - this.keys.add(key); + this.key = key; return this; } @@ -305,59 +297,47 @@ public Jwt parse(String jwt) throws ExpiredJwtException, MalformedJwtException, throw new MalformedJwtException(msg); } - if (this.keys != null && this.keyBytes != null) { + if (key != null && keyBytes != null) { throw new IllegalStateException("A key object and key bytes cannot both be specified. Choose either."); - } else if ((this.keys != null || this.keyBytes != null) && this.signingKeyResolver != null) { - String object = this.keys != null ? "a key object" : "key bytes"; + } else if ((key != null || keyBytes != null) && signingKeyResolver != null) { + String object = key != null ? "a key object" : "key bytes"; throw new IllegalStateException("A signing key resolver and " + object + " cannot both be specified. Choose either."); } //digitally signed, let's assert the signature: - Collection keys = this.keys; + Key key = this.key; - if (keys == null) { //fall back to keyBytes + if (key == null) { //fall back to keyBytes - if (Objects.isEmpty(this.keyBytes) && this.signingKeyResolver != null) { //use the signingKeyResolver - keys = new ArrayList(); + byte[] keyBytes = this.keyBytes; + + if (Objects.isEmpty(keyBytes) && signingKeyResolver != null) { //use the signingKeyResolver if (claims != null) { - Key key = this.signingKeyResolver.resolveSigningKey(jwsHeader, claims); - if (key != null) - keys.add(key); - Collection keyList = this.signingKeyResolver.resolveSigningKeys(jwsHeader, claims); - if (!Objects.isEmpty(keyList)) - keys.addAll(keyList); + key = signingKeyResolver.resolveSigningKey(jwsHeader, claims); } else { - Key key = this.signingKeyResolver.resolveSigningKey(jwsHeader, payload); - if (key != null) - keys.add(key); - Collection keyList = this.signingKeyResolver.resolveSigningKeys(jwsHeader, payload); - if (!Objects.isEmpty(keyList)) - keys.addAll(keyList); + key = signingKeyResolver.resolveSigningKey(jwsHeader, payload); } } - if (!Objects.isEmpty(this.keyBytes)) { + if (!Objects.isEmpty(keyBytes)) { Assert.isTrue(algorithm.isHmac(), "Key bytes can only be specified for HMAC signatures. Please specify a PublicKey or PrivateKey instance."); - keys = new ArrayList(); - for (byte[] bytes: this.keyBytes) - this.keys.add(new SecretKeySpec(bytes, algorithm.getJcaName())); + key = new SecretKeySpec(keyBytes, algorithm.getJcaName()); } } - Assert.notNull(keys, "A signing key must be specified if the specified JWT is digitally signed."); + Assert.notNull(key, "A signing key must be specified if the specified JWT is digitally signed."); //re-create the jwt part without the signature. This is what needs to be signed for verification: String jwtWithoutSignature = base64UrlEncodedHeader + SEPARATOR_CHAR + base64UrlEncodedPayload; JwtSignatureValidator validator; try { - validator = createSignatureValidator(algorithm, keys); + validator = createSignatureValidator(algorithm, key); } catch (IllegalArgumentException e) { String algName = algorithm.getValue(); - Key key = keys.iterator().next(); String msg = "The parsed JWT indicates it was signed with the " + algName + " signature " + "algorithm, but the specified signing key of type " + key.getClass().getName() + " may not be used to validate " + algName + " signatures. Because the specified " + @@ -488,8 +468,8 @@ private void validateExpectedClaims(Header header, Claims claims) { /* * @since 0.5 mostly to allow testing overrides */ - protected JwtSignatureValidator createSignatureValidator(SignatureAlgorithm alg, Collection keys) { - return new DefaultJwtSignatureValidator(alg, keys); + protected JwtSignatureValidator createSignatureValidator(SignatureAlgorithm alg, Key key) { + return new DefaultJwtSignatureValidator(alg, key); } @Override diff --git a/src/main/java/io/jsonwebtoken/impl/crypto/DefaultJwtSignatureValidator.java b/src/main/java/io/jsonwebtoken/impl/crypto/DefaultJwtSignatureValidator.java index 0f48a5d32..245c0d551 100644 --- a/src/main/java/io/jsonwebtoken/impl/crypto/DefaultJwtSignatureValidator.java +++ b/src/main/java/io/jsonwebtoken/impl/crypto/DefaultJwtSignatureValidator.java @@ -21,7 +21,6 @@ import java.nio.charset.Charset; import java.security.Key; -import java.util.Collection; public class DefaultJwtSignatureValidator implements JwtSignatureValidator { @@ -29,13 +28,13 @@ public class DefaultJwtSignatureValidator implements JwtSignatureValidator { private final SignatureValidator signatureValidator; - public DefaultJwtSignatureValidator(SignatureAlgorithm alg, Collection keys) { - this(DefaultSignatureValidatorFactory.INSTANCE, alg, keys); + public DefaultJwtSignatureValidator(SignatureAlgorithm alg, Key key) { + this(DefaultSignatureValidatorFactory.INSTANCE, alg, key); } - public DefaultJwtSignatureValidator(SignatureValidatorFactory factory, SignatureAlgorithm alg, Collection keys) { + public DefaultJwtSignatureValidator(SignatureValidatorFactory factory, SignatureAlgorithm alg, Key key) { Assert.notNull(factory, "SignerFactory argument cannot be null."); - this.signatureValidator = factory.createSignatureValidator(alg, keys); + this.signatureValidator = factory.createSignatureValidator(alg, key); } @Override diff --git a/src/main/java/io/jsonwebtoken/impl/crypto/DefaultSignatureValidatorFactory.java b/src/main/java/io/jsonwebtoken/impl/crypto/DefaultSignatureValidatorFactory.java index be909c6f5..82916847c 100644 --- a/src/main/java/io/jsonwebtoken/impl/crypto/DefaultSignatureValidatorFactory.java +++ b/src/main/java/io/jsonwebtoken/impl/crypto/DefaultSignatureValidatorFactory.java @@ -19,33 +19,32 @@ import io.jsonwebtoken.lang.Assert; import java.security.Key; -import java.util.Collection; public class DefaultSignatureValidatorFactory implements SignatureValidatorFactory { public static final SignatureValidatorFactory INSTANCE = new DefaultSignatureValidatorFactory(); @Override - public SignatureValidator createSignatureValidator(SignatureAlgorithm alg, Collection keys) { + public SignatureValidator createSignatureValidator(SignatureAlgorithm alg, Key key) { Assert.notNull(alg, "SignatureAlgorithm cannot be null."); - Assert.notNull(keys, "Signing Key cannot be null."); + Assert.notNull(key, "Signing Key cannot be null."); switch (alg) { case HS256: case HS384: case HS512: - return new MacValidator(alg, keys); + return new MacValidator(alg, key); case RS256: case RS384: case RS512: case PS256: case PS384: case PS512: - return new RsaSignatureValidator(alg, keys); + return new RsaSignatureValidator(alg, key); case ES256: case ES384: case ES512: - return new EllipticCurveSignatureValidator(alg, keys); + return new EllipticCurveSignatureValidator(alg, key); default: throw new IllegalArgumentException("The '" + alg.name() + "' algorithm cannot be used for signing."); } diff --git a/src/main/java/io/jsonwebtoken/impl/crypto/EllipticCurveSignatureValidator.java b/src/main/java/io/jsonwebtoken/impl/crypto/EllipticCurveSignatureValidator.java index ed548e37f..09ab14db6 100644 --- a/src/main/java/io/jsonwebtoken/impl/crypto/EllipticCurveSignatureValidator.java +++ b/src/main/java/io/jsonwebtoken/impl/crypto/EllipticCurveSignatureValidator.java @@ -20,7 +20,6 @@ import java.security.PublicKey; import java.security.Signature; import java.security.interfaces.ECPublicKey; -import java.util.Collection; import io.jsonwebtoken.SignatureAlgorithm; import io.jsonwebtoken.SignatureException; @@ -31,38 +30,30 @@ public class EllipticCurveSignatureValidator extends EllipticCurveProvider imple private static final String EC_PUBLIC_KEY_REQD_MSG = "Elliptic Curve signature validation requires an ECPublicKey instance."; - private final Collection keys; - - public EllipticCurveSignatureValidator(SignatureAlgorithm alg, Collection keys) { - super(alg, null); - this.keys = keys; - for (Key key: keys) - Assert.isTrue(key instanceof ECPublicKey, EC_PUBLIC_KEY_REQD_MSG); + public EllipticCurveSignatureValidator(SignatureAlgorithm alg, Key key) { + super(alg, key); + Assert.isTrue(key instanceof ECPublicKey, EC_PUBLIC_KEY_REQD_MSG); } @Override public boolean isValid(byte[] data, byte[] signature) { Signature sig = createSignatureInstance(); - for (Key key: this.keys) { - PublicKey publicKey = (PublicKey) key; - try { - int expectedSize = getSignatureByteArrayLength(this.alg); - /** - * - * If the expected size is not valid for JOSE, fall back to ASN.1 DER signature. - * This fallback is for backwards compatibility ONLY (to support tokens generated by previous versions of jjwt) - * and backwards compatibility will possibly be removed in a future version of this library. - * - * **/ - byte[] derSignature = expectedSize != signature.length && signature[0] == 0x30 ? signature : EllipticCurveProvider.transcodeSignatureToDER(signature); - if (doVerify(sig, publicKey, data, derSignature)) - return true; - } catch (Exception e) { - String msg = "Unable to verify Elliptic Curve signature using configured ECPublicKey. " + e.getMessage(); - throw new SignatureException(msg, e); - } + PublicKey publicKey = (PublicKey) key; + try { + int expectedSize = getSignatureByteArrayLength(alg); + /** + * + * If the expected size is not valid for JOSE, fall back to ASN.1 DER signature. + * This fallback is for backwards compatibility ONLY (to support tokens generated by previous versions of jjwt) + * and backwards compatibility will possibly be removed in a future version of this library. + * + * **/ + byte[] derSignature = expectedSize != signature.length && signature[0] == 0x30 ? signature : EllipticCurveProvider.transcodeSignatureToDER(signature); + return doVerify(sig, publicKey, data, derSignature); + } catch (Exception e) { + String msg = "Unable to verify Elliptic Curve signature using configured ECPublicKey. " + e.getMessage(); + throw new SignatureException(msg, e); } - return false; } protected boolean doVerify(Signature sig, PublicKey publicKey, byte[] data, byte[] signature) diff --git a/src/main/java/io/jsonwebtoken/impl/crypto/MacProvider.java b/src/main/java/io/jsonwebtoken/impl/crypto/MacProvider.java index 98bdd73f0..1ce280bb9 100644 --- a/src/main/java/io/jsonwebtoken/impl/crypto/MacProvider.java +++ b/src/main/java/io/jsonwebtoken/impl/crypto/MacProvider.java @@ -22,8 +22,6 @@ import javax.crypto.spec.SecretKeySpec; import java.security.Key; import java.security.SecureRandom; -import java.util.ArrayList; -import java.util.Collection; public abstract class MacProvider extends SignatureProvider { @@ -46,20 +44,6 @@ public static SecretKey generateKey() { return generateKey(SignatureAlgorithm.HS512); } - /** - * Generates a collection of new secure-random 512 bit secret key suitable for creating and verifying HMAC signatures. This is a - * convenience method that immediately delegates to {@link #generateKey(SignatureAlgorithm)} using {@link - * SignatureAlgorithm#HS512} as the method argument. - * - * @return a new secure-random 512 bit secret key suitable for creating and verifying HMAC signatures. - * @see #generateKey(SignatureAlgorithm) - * @see #generateKey(SignatureAlgorithm, SecureRandom) - * @since 0.5 - */ - public static Collection generateKeys(int howMany) { - return generateKeys(SignatureAlgorithm.HS512, howMany); - } - /** * Generates a new secure-random secret key of a length suitable for creating and verifying HMAC signatures * according to the specified {@code SignatureAlgorithm} using JJWT's default {@link @@ -78,27 +62,6 @@ public static SecretKey generateKey(SignatureAlgorithm alg) { return generateKey(alg, SignatureProvider.DEFAULT_SECURE_RANDOM); } - /** - * Generates a new secure-random secret key of a length suitable for creating and verifying HMAC signatures - * according to the specified {@code SignatureAlgorithm} using JJWT's default {@link - * SignatureProvider#DEFAULT_SECURE_RANDOM SecureRandom instance}. This is a convenience method that immediately - * delegates to {@link #generateKey(SignatureAlgorithm, SecureRandom)}. - * - * @param alg the desired signature algorithm - * @return a new secure-random secret key of a length suitable for creating and verifying HMAC signatures according - * to the specified {@code SignatureAlgorithm} using JJWT's default {@link SignatureProvider#DEFAULT_SECURE_RANDOM - * SecureRandom instance}. - * @see #generateKey() - * @see #generateKey(SignatureAlgorithm, SecureRandom) - * @since 0.5 - */ - public static Collection generateKeys(SignatureAlgorithm alg, int howMany) { - Collection keys = new ArrayList(); - for (int i=0;i signers; + private final MacSigner signer; - public MacValidator(SignatureAlgorithm alg, Collection keys) { - Collection signers = new ArrayList(); - for (Key key: keys) - signers.add(new MacSigner(alg, key)); - this.signers = signers; + public MacValidator(SignatureAlgorithm alg, Key key) { + this.signer = new MacSigner(alg, key); } @Override public boolean isValid(byte[] data, byte[] signature) { - for (MacSigner signer: this.signers) { - byte[] computed = signer.sign(data); - if (MessageDigest.isEqual(computed, signature)) - return true; - } - return false; + byte[] computed = this.signer.sign(data); + return MessageDigest.isEqual(computed, signature); } } diff --git a/src/main/java/io/jsonwebtoken/impl/crypto/RsaSignatureValidator.java b/src/main/java/io/jsonwebtoken/impl/crypto/RsaSignatureValidator.java index 379d846a5..a306d748f 100644 --- a/src/main/java/io/jsonwebtoken/impl/crypto/RsaSignatureValidator.java +++ b/src/main/java/io/jsonwebtoken/impl/crypto/RsaSignatureValidator.java @@ -25,58 +25,35 @@ import java.security.Signature; import java.security.interfaces.RSAPrivateKey; import java.security.interfaces.RSAPublicKey; -import java.util.ArrayList; import java.util.Arrays; -import java.util.Collection; public class RsaSignatureValidator extends RsaProvider implements SignatureValidator { - private final Collection SIGNERS; + private final RsaSigner SIGNER; - public static final class SignerAndKey { - - private final RsaSigner signer; - private final Key key; - - public SignerAndKey(final RsaSigner signer, final Key key) { - this.signer = signer; - this.key = key; - } - } - - public RsaSignatureValidator(SignatureAlgorithm alg, Collection keys) { - super(alg, null); - - Collection SIGNERS = new ArrayList(); - for (Key key: keys) { - Assert.isTrue(key instanceof RSAPrivateKey || key instanceof RSAPublicKey, - "RSA Signature validation requires either a RSAPublicKey or RSAPrivateKey instance."); - SIGNERS.add(new SignerAndKey(new RsaSigner(alg, key), key)); - } - this.SIGNERS = SIGNERS; + public RsaSignatureValidator(SignatureAlgorithm alg, Key key) { + super(alg, key); + Assert.isTrue(key instanceof RSAPrivateKey || key instanceof RSAPublicKey, + "RSA Signature validation requires either a RSAPublicKey or RSAPrivateKey instance."); + this.SIGNER = key instanceof RSAPrivateKey ? new RsaSigner(alg, key) : null; } @Override public boolean isValid(byte[] data, byte[] signature) { - for (SignerAndKey signerAndKey: this.SIGNERS) { - if (signerAndKey.key instanceof PublicKey) { - Signature sig = createSignatureInstance(); - PublicKey publicKey = (PublicKey) signerAndKey.key; - try { - if (doVerify(sig, publicKey, data, signature)) - return true; - } catch (Exception e) { - String msg = "Unable to verify RSA signature using configured PublicKey. " + e.getMessage(); - throw new SignatureException(msg, e); - } - } else { - Assert.notNull(this.SIGNERS, "RSA Signer instance cannot be null. This is a bug. Please report it."); - byte[] computed = signerAndKey.signer.sign(data); - if (Arrays.equals(computed, signature)) - return true; + if (key instanceof PublicKey) { + Signature sig = createSignatureInstance(); + PublicKey publicKey = (PublicKey) key; + try { + return doVerify(sig, publicKey, data, signature); + } catch (Exception e) { + String msg = "Unable to verify RSA signature using configured PublicKey. " + e.getMessage(); + throw new SignatureException(msg, e); } + } else { + Assert.notNull(this.SIGNER, "RSA Signer instance cannot be null. This is a bug. Please report it."); + byte[] computed = this.SIGNER.sign(data); + return Arrays.equals(computed, signature); } - return false; } protected boolean doVerify(Signature sig, PublicKey publicKey, byte[] data, byte[] signature) diff --git a/src/main/java/io/jsonwebtoken/impl/crypto/SignatureValidatorFactory.java b/src/main/java/io/jsonwebtoken/impl/crypto/SignatureValidatorFactory.java index 7712f4a4d..1e84b620e 100644 --- a/src/main/java/io/jsonwebtoken/impl/crypto/SignatureValidatorFactory.java +++ b/src/main/java/io/jsonwebtoken/impl/crypto/SignatureValidatorFactory.java @@ -18,9 +18,8 @@ import io.jsonwebtoken.SignatureAlgorithm; import java.security.Key; -import java.util.Collection; public interface SignatureValidatorFactory { - SignatureValidator createSignatureValidator(SignatureAlgorithm alg, Collection keys); + SignatureValidator createSignatureValidator(SignatureAlgorithm alg, Key key); } diff --git a/src/main/java/io/jsonwebtoken/lang/Objects.java b/src/main/java/io/jsonwebtoken/lang/Objects.java index 47cd81207..eb475ac69 100644 --- a/src/main/java/io/jsonwebtoken/lang/Objects.java +++ b/src/main/java/io/jsonwebtoken/lang/Objects.java @@ -19,7 +19,6 @@ import java.io.IOException; import java.lang.reflect.Array; import java.util.Arrays; -import java.util.Collection; public final class Objects { @@ -96,16 +95,6 @@ public static boolean isEmpty(Object[] array) { return (array == null || array.length == 0); } - /** - * Determine whether the given collection is empty: - * i.e. null or of zero length. - * - * @param array the Collection to check - */ - public static boolean isEmpty(Collection collection) { - return (collection == null || collection.isEmpty()); - } - /** * Returns {@code true} if the specified byte array is null or of zero length, {@code false} otherwise. * diff --git a/src/test/groovy/io/jsonwebtoken/impl/crypto/DefaultSignatureValidatorFactoryTest.groovy b/src/test/groovy/io/jsonwebtoken/impl/crypto/DefaultSignatureValidatorFactoryTest.groovy index 5032ead49..04ec851d7 100644 --- a/src/test/groovy/io/jsonwebtoken/impl/crypto/DefaultSignatureValidatorFactoryTest.groovy +++ b/src/test/groovy/io/jsonwebtoken/impl/crypto/DefaultSignatureValidatorFactoryTest.groovy @@ -24,7 +24,7 @@ class DefaultSignatureValidatorFactoryTest { @Test void testNoneAlgorithm() { try { - new DefaultSignatureValidatorFactory().createSignatureValidator(SignatureAlgorithm.NONE, MacProvider.generateKeys(1)) + new DefaultSignatureValidatorFactory().createSignatureValidator(SignatureAlgorithm.NONE, MacProvider.generateKey()) fail() } catch (IllegalArgumentException iae) { assertEquals iae.message, "The 'NONE' algorithm cannot be used for signing." From 6396313774c56aa90f97f6f171bb23331b8299e6 Mon Sep 17 00:00:00 2001 From: Paul Cook Date: Thu, 27 Jul 2017 18:39:31 +0100 Subject: [PATCH 19/19] supporting multiple public keys in SigningKeyResolver --- .../io/jsonwebtoken/SigningKeyResolver.java | 24 ++ .../SigningKeyResolverAdapter.java | 75 +++++ .../jsonwebtoken/impl/DefaultJwtParser.java | 109 +++++-- .../java/io/jsonwebtoken/lang/Objects.java | 11 + .../io/jsonwebtoken/JwtParserTest.groovy | 289 +++++++++++++++++- 5 files changed, 476 insertions(+), 32 deletions(-) diff --git a/src/main/java/io/jsonwebtoken/SigningKeyResolver.java b/src/main/java/io/jsonwebtoken/SigningKeyResolver.java index b068db9d2..789a9c2c8 100644 --- a/src/main/java/io/jsonwebtoken/SigningKeyResolver.java +++ b/src/main/java/io/jsonwebtoken/SigningKeyResolver.java @@ -16,6 +16,7 @@ package io.jsonwebtoken; import java.security.Key; +import java.util.Collection; /** * A {@code SigningKeyResolver} can be used by a {@link io.jsonwebtoken.JwtParser JwtParser} to find a signing key that @@ -60,6 +61,17 @@ public interface SigningKeyResolver { */ Key resolveSigningKey(JwsHeader header, Claims claims); + /** + * Returns a collection signing key that should be used to attempt to validate a digital signature for the Claims JWS with the specified + * header and claims. This allows for a key rotation scenario to support multiple keys during an overlap period. + * + * @param header the header of the JWS to validate + * @param claims the claims (body) of the JWS to validate + * @return the signing key that should be used to validate a digital signature for the Claims JWS with the specified + * header and claims. + */ + Collection resolveSigningKeys(JwsHeader header, Claims claims); + /** * Returns the signing key that should be used to validate a digital signature for the Plaintext JWS with the * specified header and plaintext payload. @@ -70,4 +82,16 @@ public interface SigningKeyResolver { * specified header and plaintext payload. */ Key resolveSigningKey(JwsHeader header, String plaintext); + + /** + * Returns the signing key that should be used to attempt to validate a digital signature for the Plaintext JWS with the + * specified header and plaintext payload. This allows for a key rotation scenario to support multiple keys during an overlap period. + * + * @param header the header of the JWS to validate + * @param plaintext the plaintext body of the JWS to validate + * @return the signing key that should be used to validate a digital signature for the Plaintext JWS with the + * specified header and plaintext payload. + */ + Collection resolveSigningKeys(JwsHeader header, String plaintext); + } diff --git a/src/main/java/io/jsonwebtoken/SigningKeyResolverAdapter.java b/src/main/java/io/jsonwebtoken/SigningKeyResolverAdapter.java index 1be7ec556..53da6db44 100644 --- a/src/main/java/io/jsonwebtoken/SigningKeyResolverAdapter.java +++ b/src/main/java/io/jsonwebtoken/SigningKeyResolverAdapter.java @@ -19,6 +19,8 @@ import javax.crypto.spec.SecretKeySpec; import java.security.Key; +import java.util.ArrayList; +import java.util.Collection; /** * An Adapter implementation of the @@ -48,9 +50,28 @@ public Key resolveSigningKey(JwsHeader header, Claims claims) { "Override the resolveSigningKey(JwsHeader, Claims) method instead and return a " + "Key instance appropriate for the " + alg.name() + " algorithm."); byte[] keyBytes = resolveSigningKeyBytes(header, claims); + if (keyBytes == null) + return null; return new SecretKeySpec(keyBytes, alg.getJcaName()); } + @Override + public Collection resolveSigningKeys(JwsHeader header, Claims claims) { + SignatureAlgorithm alg = SignatureAlgorithm.forName(header.getAlgorithm()); + Assert.isTrue(alg.isHmac(), "The default resolveSigningKey(JwsHeader, Claims) implementation cannot be " + + "used for asymmetric key algorithms (RSA, Elliptic Curve). " + + "Override the resolveSigningKey(JwsHeader, Claims) method instead and return a " + + "Key instance appropriate for the " + alg.name() + " algorithm."); + Collection keysBytes = resolveSigningKeysBytes(header, claims); + if (keysBytes == null) + return null; + Collection keys = new ArrayList(); + for (byte[] keyBytes: keysBytes) + keys.add(new SecretKeySpec(keyBytes, alg.getJcaName())); + + return keys; + } + @Override public Key resolveSigningKey(JwsHeader header, String plaintext) { SignatureAlgorithm alg = SignatureAlgorithm.forName(header.getAlgorithm()); @@ -59,9 +80,28 @@ public Key resolveSigningKey(JwsHeader header, String plaintext) { "Override the resolveSigningKey(JwsHeader, String) method instead and return a " + "Key instance appropriate for the " + alg.name() + " algorithm."); byte[] keyBytes = resolveSigningKeyBytes(header, plaintext); + if (keyBytes == null) + return null; return new SecretKeySpec(keyBytes, alg.getJcaName()); } + @Override + public Collection resolveSigningKeys(JwsHeader header, String plaintext) { + SignatureAlgorithm alg = SignatureAlgorithm.forName(header.getAlgorithm()); + Assert.isTrue(alg.isHmac(), "The default resolveSigningKey(JwsHeader, String) implementation cannot be " + + "used for asymmetric key algorithms (RSA, Elliptic Curve). " + + "Override the resolveSigningKey(JwsHeader, String) method instead and return a " + + "Key instance appropriate for the " + alg.name() + " algorithm."); + Collection keysBytes = resolveSigningKeysBytes(header, plaintext); + if (keysBytes == null) + return null; + Collection keys = new ArrayList(); + for (byte[] keyBytes: keysBytes) + keys.add(new SecretKeySpec(keyBytes, alg.getJcaName())); + + return keys; + } + /** * Convenience method invoked by {@link #resolveSigningKey(JwsHeader, Claims)} that obtains the necessary signing * key bytes. This implementation simply throws an exception: if the JWS parsed is a Claims JWS, you must @@ -81,6 +121,25 @@ public byte[] resolveSigningKeyBytes(JwsHeader header, Claims claims) { "resolveSigningKeyBytes(JwsHeader, Claims) method."); } + /** + * Convenience method invoked by {@link #resolveSigningKey(JwsHeader, Claims)} that obtains the necessary signing + * key bytes. This implementation simply throws an exception: if the JWS parsed is a Claims JWS, you must + * override this method or the {@link #resolveSigningKey(JwsHeader, Claims)} method instead. + * + *

NOTE: You cannot override this method when validating RSA signatures. If you expect RSA signatures, + * you must override the {@link #resolveSigningKey(JwsHeader, Claims)} method instead.

+ * + * @param header the parsed {@link JwsHeader} + * @param claims the parsed {@link Claims} + * @return the signing key bytes to use to verify the JWS signature. + */ + public Collection resolveSigningKeysBytes(JwsHeader header, Claims claims) { + throw new UnsupportedJwtException("The specified SigningKeyResolver implementation does not support " + + "Claims JWS signing key resolution. Consider overriding either the " + + "resolveSigningKey(JwsHeader, Claims) method or, for HMAC algorithms, the " + + "resolveSigningKeyBytes(JwsHeader, Claims) method."); + } + /** * Convenience method invoked by {@link #resolveSigningKey(JwsHeader, String)} that obtains the necessary signing * key bytes. This implementation simply throws an exception: if the JWS parsed is a plaintext JWS, you must @@ -96,4 +155,20 @@ public byte[] resolveSigningKeyBytes(JwsHeader header, String payload) { "resolveSigningKey(JwsHeader, String) method or, for HMAC algorithms, the " + "resolveSigningKeyBytes(JwsHeader, String) method."); } + + /** + * Convenience method invoked by {@link #resolveSigningKey(JwsHeader, String)} that obtains the necessary signing + * key bytes. This implementation simply throws an exception: if the JWS parsed is a plaintext JWS, you must + * override this method or the {@link #resolveSigningKey(JwsHeader, String)} method instead. + * + * @param header the parsed {@link JwsHeader} + * @param payload the parsed String plaintext payload + * @return the signing key bytes to use to verify the JWS signature. + */ + public Collection resolveSigningKeysBytes(JwsHeader header, String payload) { + throw new UnsupportedJwtException("The specified SigningKeyResolver implementation does not support " + + "plaintext JWS signing key resolution. Consider overriding either the " + + "resolveSigningKey(JwsHeader, String) method or, for HMAC algorithms, the " + + "resolveSigningKeyBytes(JwsHeader, String) method."); + } } diff --git a/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java b/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java index 4e4b9c79c..937c74029 100644 --- a/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java +++ b/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java @@ -49,6 +49,8 @@ import java.io.IOException; import java.security.Key; import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Collection; import java.util.Date; import java.util.Map; @@ -61,9 +63,9 @@ public class DefaultJwtParser implements JwtParser { private ObjectMapper objectMapper = new ObjectMapper(); - private byte[] keyBytes; + private Collection keyBytes; - private Key key; + private Collection keys; private SigningKeyResolver signingKeyResolver; @@ -141,21 +143,27 @@ public JwtParser setAllowedClockSkewSeconds(long seconds) { @Override public JwtParser setSigningKey(byte[] key) { Assert.notEmpty(key, "signing key cannot be null or empty."); - this.keyBytes = key; + if (this.keyBytes == null) + this.keyBytes = new ArrayList(); + this.keyBytes.add(key); return this; } @Override public JwtParser setSigningKey(String base64EncodedKeyBytes) { Assert.hasText(base64EncodedKeyBytes, "signing key cannot be null or empty."); - this.keyBytes = TextCodec.BASE64.decode(base64EncodedKeyBytes); + if (this.keyBytes == null) + this.keyBytes = new ArrayList(); + this.keyBytes.add(TextCodec.BASE64.decode(base64EncodedKeyBytes)); return this; } @Override public JwtParser setSigningKey(Key key) { Assert.notNull(key, "signing key cannot be null."); - this.key = key; + if (this.keys == null) + this.keys = new ArrayList(); + this.keys.add(key); return this; } @@ -297,26 +305,56 @@ public Jwt parse(String jwt) throws ExpiredJwtException, MalformedJwtException, throw new MalformedJwtException(msg); } - if (key != null && keyBytes != null) { + if (keys != null && keyBytes != null) { throw new IllegalStateException("A key object and key bytes cannot both be specified. Choose either."); - } else if ((key != null || keyBytes != null) && signingKeyResolver != null) { - String object = key != null ? "a key object" : "key bytes"; + } else if ((keys != null || keyBytes != null) && signingKeyResolver != null) { + String object = keys != null ? "a key object" : "key bytes"; throw new IllegalStateException("A signing key resolver and " + object + " cannot both be specified. Choose either."); } //digitally signed, let's assert the signature: - Key key = this.key; + Collection keys = this.keys; + UnsupportedJwtException signingKeyResolverException = null; - if (key == null) { //fall back to keyBytes + if (keys == null) { //fall back to keyBytes - byte[] keyBytes = this.keyBytes; + //byte[] keyBytes = this.keyBytes; if (Objects.isEmpty(keyBytes) && signingKeyResolver != null) { //use the signingKeyResolver + keys = new ArrayList(); if (claims != null) { - key = signingKeyResolver.resolveSigningKey(jwsHeader, claims); + try { + Key key = this.signingKeyResolver.resolveSigningKey(jwsHeader, claims); + if (key != null) + keys.add(key); + } catch (UnsupportedJwtException e) { + signingKeyResolverException = e; + } + try { + Collection keyList = this.signingKeyResolver.resolveSigningKeys(jwsHeader, claims); + if (!Objects.isEmpty(keyList)) + keys.addAll(keyList); + } catch (UnsupportedJwtException e) { + signingKeyResolverException = e; + } } else { - key = signingKeyResolver.resolveSigningKey(jwsHeader, payload); + try { + Key key = this.signingKeyResolver.resolveSigningKey(jwsHeader, payload); + if (key != null) + keys.add(key); + } catch (UnsupportedJwtException e) { + signingKeyResolverException = e; + } + try { + Collection keyList = this.signingKeyResolver.resolveSigningKeys(jwsHeader, payload); + if (!Objects.isEmpty(keyList)) + keys.addAll(keyList); + } catch (UnsupportedJwtException e) { + signingKeyResolverException = e; + } } + if (keys.size() == 0 && signingKeyResolverException != null) + throw signingKeyResolverException; } if (!Objects.isEmpty(keyBytes)) { @@ -324,31 +362,40 @@ public Jwt parse(String jwt) throws ExpiredJwtException, MalformedJwtException, Assert.isTrue(algorithm.isHmac(), "Key bytes can only be specified for HMAC signatures. Please specify a PublicKey or PrivateKey instance."); - key = new SecretKeySpec(keyBytes, algorithm.getJcaName()); + keys = new ArrayList(); + for (byte[] bytes: this.keyBytes) + keys.add(new SecretKeySpec(bytes, algorithm.getJcaName())); } } - - Assert.notNull(key, "A signing key must be specified if the specified JWT is digitally signed."); + + Assert.notNull(keys, "A signing key must be specified if the specified JWT is digitally signed."); //re-create the jwt part without the signature. This is what needs to be signed for verification: String jwtWithoutSignature = base64UrlEncodedHeader + SEPARATOR_CHAR + base64UrlEncodedPayload; - JwtSignatureValidator validator; - try { - validator = createSignatureValidator(algorithm, key); - } catch (IllegalArgumentException e) { - String algName = algorithm.getValue(); - String msg = "The parsed JWT indicates it was signed with the " + algName + " signature " + - "algorithm, but the specified signing key of type " + key.getClass().getName() + - " may not be used to validate " + algName + " signatures. Because the specified " + - "signing key reflects a specific and expected algorithm, and the JWT does not reflect " + - "this algorithm, it is likely that the JWT was not expected and therefore should not be " + - "trusted. Another possibility is that the parser was configured with the incorrect " + - "signing key, but this cannot be assumed for security reasons."; - throw new UnsupportedJwtException(msg, e); + boolean signatureOk = false; + for (Key key: keys) { + JwtSignatureValidator validator; + try { + validator = createSignatureValidator(algorithm, key); + } catch (IllegalArgumentException e) { + String algName = algorithm.getValue(); + String msg = "The parsed JWT indicates it was signed with the " + algName + " signature " + + "algorithm, but the specified signing key of type " + key.getClass().getName() + + " may not be used to validate " + algName + " signatures. Because the specified " + + "signing key reflects a specific and expected algorithm, and the JWT does not reflect " + + "this algorithm, it is likely that the JWT was not expected and therefore should not be " + + "trusted. Another possibility is that the parser was configured with the incorrect " + + "signing key, but this cannot be assumed for security reasons."; + throw new UnsupportedJwtException(msg, e); + } + + if (validator.isValid(jwtWithoutSignature, base64UrlEncodedDigest)) { + signatureOk = true; + break; + } } - - if (!validator.isValid(jwtWithoutSignature, base64UrlEncodedDigest)) { + if (!signatureOk) { String msg = "JWT signature does not match locally computed signature. JWT validity cannot be " + "asserted and should not be trusted."; throw new SignatureException(msg); diff --git a/src/main/java/io/jsonwebtoken/lang/Objects.java b/src/main/java/io/jsonwebtoken/lang/Objects.java index eb475ac69..3038b5bb9 100644 --- a/src/main/java/io/jsonwebtoken/lang/Objects.java +++ b/src/main/java/io/jsonwebtoken/lang/Objects.java @@ -19,6 +19,7 @@ import java.io.IOException; import java.lang.reflect.Array; import java.util.Arrays; +import java.util.Collection; public final class Objects { @@ -95,6 +96,16 @@ public static boolean isEmpty(Object[] array) { return (array == null || array.length == 0); } + /** + * Determine whether the given collection is empty: + * i.e. null or of zero length. + * + * @param array the Collection to check + */ + public static boolean isEmpty(Collection collection) { + return (collection == null || collection.isEmpty()); + } + /** * Returns {@code true} if the specified byte array is null or of zero length, {@code false} otherwise. * diff --git a/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy b/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy index 187711fec..17071a7ae 100644 --- a/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy +++ b/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy @@ -606,6 +606,35 @@ class JwtParserTest { assertEquals jws.getBody().getSubject(), subject } + @Test + void testParseClaimsWithSigningKeysResolver() { + + String subject = 'Joe' + + byte[] key = randomKey() + byte[] key2 = randomKey() + + String compact = Jwts.builder().setSubject(subject).signWith(SignatureAlgorithm.HS256, key).compact() + + def signingKeyResolver = new SigningKeyResolverAdapter() { + @Override + byte[] resolveSigningKeyBytes(JwsHeader header, Claims claims) { + return null + } + @Override + Collection resolveSigningKeysBytes(JwsHeader header, Claims claims) { + ArrayList list = new ArrayList() + list.add(key2) + list.add(key) + return list + } + } + + Jws jws = Jwts.parser().setSigningKeyResolver(signingKeyResolver).parseClaimsJws(compact) + + assertEquals jws.getBody().getSubject(), subject + } + @Test void testParseClaimsWithSigningKeyResolverInvalidKey() { @@ -630,6 +659,37 @@ class JwtParserTest { } } + @Test + void testParseClaimsWithSigningKeyResolverInvalidKeys() { + + String subject = 'Joe' + + byte[] key = randomKey() + + String compact = Jwts.builder().setSubject(subject).signWith(SignatureAlgorithm.HS256, key).compact() + + def signingKeyResolver = new SigningKeyResolverAdapter() { + @Override + byte[] resolveSigningKeyBytes(JwsHeader header, Claims claims) { + return null + } + @Override + Collection resolveSigningKeysBytes(JwsHeader header, Claims claims) { + ArrayList list = new ArrayList() + list.add(randomKey()) + list.add(randomKey()) + return list + } + } + + try { + Jwts.parser().setSigningKeyResolver(signingKeyResolver).parseClaimsJws(compact) + fail() + } catch (SignatureException se) { + assertEquals se.getMessage(), 'JWT signature does not match locally computed signature. JWT validity cannot be asserted and should not be trusted.' + } + } + @Test void testParseClaimsWithSigningKeyResolverAndKey() { @@ -654,6 +714,37 @@ class JwtParserTest { } } + @Test + void testParseClaimsWithSigningKeyResolverAndKeys() { + + String subject = 'Joe' + + SecretKeySpec key = new SecretKeySpec(randomKey(), "HmacSHA256") + + String compact = Jwts.builder().setSubject(subject).signWith(SignatureAlgorithm.HS256, key).compact() + + def signingKeyResolver = new SigningKeyResolverAdapter() { + @Override + byte[] resolveSigningKeyBytes(JwsHeader header, Claims claims) { + return null + } + @Override + Collection resolveSigningKeysBytes(JwsHeader header, Claims claims) { + ArrayList list = new ArrayList() + list.add(randomKey()) + list.add(randomKey()) + return list + } + } + + try { + Jwts.parser().setSigningKey(key).setSigningKeyResolver(signingKeyResolver).parseClaimsJws(compact) + fail() + } catch (IllegalStateException ise) { + assertEquals ise.getMessage(), 'A signing key resolver and a key object cannot both be specified. Choose either.' + } + } + @Test void testParseClaimsWithSigningKeyResolverAndKeyBytes() { @@ -678,6 +769,37 @@ class JwtParserTest { } } + @Test + void testParseClaimsWithSigningKeyResolverAndKeysBytes() { + + String subject = 'Joe' + + byte[] key = randomKey() + + String compact = Jwts.builder().setSubject(subject).signWith(SignatureAlgorithm.HS256, key).compact() + + def signingKeyResolver = new SigningKeyResolverAdapter() { + @Override + byte[] resolveSigningKeyBytes(JwsHeader header, Claims claims) { + return null + } + @Override + Collection resolveSigningKeysBytes(JwsHeader header, Claims claims) { + ArrayList list = new ArrayList() + list.add(randomKey()) + list.add(randomKey()) + return list + } + } + + try { + Jwts.parser().setSigningKey(key).setSigningKeyResolver(signingKeyResolver).parseClaimsJws(compact) + fail() + } catch (IllegalStateException ise) { + assertEquals ise.getMessage(), 'A signing key resolver and key bytes cannot both be specified. Choose either.' + } + } + @Test void testParseClaimsWithNullSigningKeyResolver() { @@ -716,6 +838,59 @@ class JwtParserTest { } } + @Test + void testParseClaimsWithSigningKeyResolverAdapterWithNoKey() { + + + String subject = 'Joe' + + byte[] key = randomKey() + + String compact = Jwts.builder().setSubject(subject).signWith(SignatureAlgorithm.HS256, key).compact() + + def signingKeyResolver = new SigningKeyResolverAdapter() { + @Override + byte[] resolveSigningKeyBytes(JwsHeader header, Claims claims) { + return null + } + } + + try { + Jwts.parser().setSigningKeyResolver(signingKeyResolver).parseClaimsJws(compact) + fail() + } catch (UnsupportedJwtException ex) { + assertEquals ex.getMessage(), 'The specified SigningKeyResolver implementation does not support ' + + 'Claims JWS signing key resolution. Consider overriding either the resolveSigningKey(JwsHeader, Claims) method ' + + 'or, for HMAC algorithms, the resolveSigningKeyBytes(JwsHeader, Claims) method.' + } + } + + @Test + void testParseClaimsWithSigningKeyResolverAdapterWithNoKeys() { + + String subject = 'Joe' + + byte[] key = randomKey() + + String compact = Jwts.builder().setSubject(subject).signWith(SignatureAlgorithm.HS256, key).compact() + + def signingKeyResolver = new SigningKeyResolverAdapter() { + @Override + Collection resolveSigningKeysBytes(JwsHeader header, Claims claims) { + return null + } + } + + try { + Jwts.parser().setSigningKeyResolver(signingKeyResolver).parseClaimsJws(compact) + fail() + } catch (UnsupportedJwtException ex) { + assertEquals ex.getMessage(), 'The specified SigningKeyResolver implementation does not support ' + + 'Claims JWS signing key resolution. Consider overriding either the resolveSigningKey(JwsHeader, Claims) method ' + + 'or, for HMAC algorithms, the resolveSigningKeyBytes(JwsHeader, Claims) method.' + } + } + @Test void testParseClaimsJwsWithNumericTypes() { byte[] key = randomKey() @@ -772,6 +947,35 @@ class JwtParserTest { assertEquals jws.getBody(), inputPayload } + @Test + void testParsePlaintextJwsWithSigningKeyResolverAdapterMultipleKeys() { + + String inputPayload = 'Hello world!' + + byte[] key = randomKey() + byte[] key2 = randomKey() + + String compact = Jwts.builder().setPayload(inputPayload).signWith(SignatureAlgorithm.HS256, key).compact() + + def signingKeyResolver = new SigningKeyResolverAdapter() { + @Override + byte[] resolveSigningKeyBytes(JwsHeader header, String payload) { + return null + } + @Override + Collection resolveSigningKeysBytes(JwsHeader header, String payload) { + ArrayList list = new ArrayList() + list.add(key2) + list.add(key) + return list + } + } + + Jws jws = Jwts.parser().setSigningKeyResolver(signingKeyResolver).parsePlaintextJws(compact) + + assertEquals jws.getBody(), inputPayload + } + @Test void testParsePlaintextJwsWithSigningKeyResolverInvalidKey() { @@ -796,6 +1000,37 @@ class JwtParserTest { } } + @Test + void testParsePlaintextJwsWithSigningKeyResolverInvalidMultipleKey() { + + String inputPayload = 'Hello world!' + + byte[] key = randomKey() + + String compact = Jwts.builder().setPayload(inputPayload).signWith(SignatureAlgorithm.HS256, key).compact() + + def signingKeyResolver = new SigningKeyResolverAdapter() { + @Override + byte[] resolveSigningKeyBytes(JwsHeader header, String payload) { + return null + } + @Override + Collection resolveSigningKeysBytes(JwsHeader header, String payload) { + ArrayList list = new ArrayList() + list.add(randomKey()) + list.add(randomKey()) + return list + } + } + + try { + Jwts.parser().setSigningKeyResolver(signingKeyResolver).parsePlaintextJws(compact) + fail() + } catch (SignatureException se) { + assertEquals se.getMessage(), 'JWT signature does not match locally computed signature. JWT validity cannot be asserted and should not be trusted.' + } + } + @Test void testParsePlaintextJwsWithInvalidSigningKeyResolverAdapter() { @@ -817,6 +1052,58 @@ class JwtParserTest { } } + @Test + void testParsePlaintextJwsWithSigningKeyResolverAdapterWithNoKey() { + + String inputPayload = 'Hello world!' + + byte[] key = randomKey() + + String compact = Jwts.builder().setPayload(inputPayload).signWith(SignatureAlgorithm.HS256, key).compact() + + def signingKeyResolver = new SigningKeyResolverAdapter() { + @Override + byte[] resolveSigningKeyBytes(JwsHeader header, String payload) { + return null + } + } + + try { + Jwts.parser().setSigningKeyResolver(signingKeyResolver).parsePlaintextJws(compact) + fail() + } catch (UnsupportedJwtException ex) { + assertEquals ex.getMessage(), 'The specified SigningKeyResolver implementation does not support plaintext ' + + 'JWS signing key resolution. Consider overriding either the resolveSigningKey(JwsHeader, String) ' + + 'method or, for HMAC algorithms, the resolveSigningKeyBytes(JwsHeader, String) method.' + } + } + + @Test + void testParsePlaintextJwsWithSigningKeyResolverAdapterWithNoKeys() { + + String inputPayload = 'Hello world!' + + byte[] key = randomKey() + + String compact = Jwts.builder().setPayload(inputPayload).signWith(SignatureAlgorithm.HS256, key).compact() + + def signingKeyResolver = new SigningKeyResolverAdapter() { + @Override + Collection resolveSigningKeysBytes(JwsHeader header, String payload) { + return null + } + } + + try { + Jwts.parser().setSigningKeyResolver(signingKeyResolver).parsePlaintextJws(compact) + fail() + } catch (UnsupportedJwtException ex) { + assertEquals ex.getMessage(), 'The specified SigningKeyResolver implementation does not support plaintext ' + + 'JWS signing key resolution. Consider overriding either the resolveSigningKey(JwsHeader, String) ' + + 'method or, for HMAC algorithms, the resolveSigningKeyBytes(JwsHeader, String) method.' + } + } + @Test void testParseRequireDontAllowNullClaimName() { def expectedClaimValue = 'A Most Awesome Claim Value' @@ -1286,7 +1573,7 @@ class JwtParserTest { @Test void testParseRequireExpiration_Success() { // expire in the future - def expiration = new Date(System.currentTimeMillis() + 10000) + Date expiration = new Date(System.currentTimeMillis() + 10000) byte[] key = randomKey()